小言_互联网的博客

Java多线程详解

503人阅读  评论(0)

目录

一、程序、进程、线程

二、并行和并发的概念 

三、用继承Thread类的方式创建多线程 

四、用实现Runnable接口的方式创建多线程

五、 继承方式和实现方式的联系与区别

六、线程的生命周期

七、线程安全问题 

八、线程安全问题的解决——Synchronized同步机制

九、线程安全的单例模式之懒汉式

十、死锁问题

十一、线程安全问题的解决——Lock锁 

十二、Lock锁和Synchronized的比较

十三、wait()、notify()、notifyAll()

十四、sleep()和wait()的异同

十五、用实现Callable接口的方式创建多线程

十六、使用线程池创建多线程


 

一、程序、进程、线程

  1. 程序(program):是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
  2. 进程(process):是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程,有它自身的生命周期。
  3. 线程(thread):进程可进一步细化为线程,是一个程序内部的一条执行路径。一个进程可以包含多个线程

二、并行和并发的概念 

  1. 并行:是指同一时刻多个任务同时在运行,是真意义上的同时运行
  2. 并发:是指多个任务交替使用CPU,从这个时间段上看似乎这些任务在同时运行,但实际上在某一时刻只有一个任务在运行。

三、用继承Thread类的方式创建多线程 

步骤:

  1. 定义一个子类继承Thread类。
  2. 子类中重写run()方法。
  3. 在主线程中new一个子类对象。
  4. 调用该子类对象的start()方法。

下面在main中创建了一个新的线程用于遍历0——50的偶数 


  
  1. package com.hedong;
  2. //自定义一个子类继承Thread类
  3. class FirstThread extends Thread{
  4. //重写run方法
  5. @Override
  6. public void run() {
  7. //遍历输出0——50的偶数
  8. for ( int i = 0; i < 50; i++) {
  9. if(i % 2 == 0){
  10. System.out.println(i);
  11. }
  12. }
  13. }
  14. }
  15. /**
  16. * @author hedong
  17. * @version 1.0
  18. * @date 2020/4/8 8:21
  19. */
  20. public class MyThread {
  21. public static void main(String[] args) {
  22. //创建子类对象,即一个线程对象
  23. FirstThread firstThread= new FirstThread();
  24. //调用线程对相爱那个的start方法
  25. firstThread.start();
  26. }
  27. }

注意:

  1. 想要启动多线程,必须调用start()方法。如果手动将调用start()方法的地方改为调用run()方法,那么程序将不会启动多线程模式。
  2.  run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
  3. 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出异常“IllegalThreadStateException”。

四、用实现Runnable接口的方式创建多线程

步骤:

  1. 定义一个子类实现Runnable接口。
  2. 子类中重写Runnable的run()方法。
  3. 在主线程中new一个子类对象。
  4. 将子类对象作为实际参数传递给Thread类的含参构造器中创建线程对象。
  5. 调用该线程对象的start()方法。

   
  1. package com.hedong;
  2. //自定义一个子类实现Runnable接口
  3. class FirstThread implements Runnable{
  4. //重写run方法
  5. @Override
  6. public void run() {
  7. //遍历输出0——50的偶数
  8. for ( int i = 0; i < 50; i++) {
  9. if(i % 2 == 0){
  10. System.out.println(i);
  11. }
  12. }
  13. }
  14. }
  15. /**
  16. * @author hedong
  17. * @version 1.0
  18. * @date 2020/4/8 8:21
  19. */
  20. public class MyThread {
  21. public static void main(String[] args) {
  22. //创建子类对象
  23. FirstThread firstThread= new FirstThread();
  24. //将子类对象作为一个参数放入Thread的含参构造器中,生成一个线程对象
  25. Thread myThread= new Thread(firstThread);
  26. //调用线程对象的start方法
  27. myThread.start();
  28. }
  29. }

五、 继承方式和实现方式的联系与区别

联系:通过源码可以发现Thread类实际上也实现了Runnable接口。

区别:继承Thread类的方式是将线程代码放在Thread子类的run()方法中。而实现Runnable接口的方式是将线程代码放在Runnable接口子类的run()方法中。

实现Runnable接口的方式的优点:用此种方式实现的多个线程可以共享实现Runnable接口子类中定义的对象。简单来说就是多个线程共用同一份数据资源。

六、线程的生命周期

七、线程安全问题 

问题举例:假如卡里原先有3000元,A、B两人同时取这张卡里的钱,两人的操作视作两个不同的线程。A打算取2000元,线程A进入if判断3000>2000后发生阻塞,此时B打算也取2000元,并且线程B在线程A发生阻塞的时候顺利取走了2000元,此时卡里只剩1000元,等线程A阻塞结束后继续扣去卡里2000元,这时悲剧发生:卡里最终金额为-1000。

问题的原因: 当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行,导致共享数据的错误。
 
解决办法: 对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。Java对于多线程的安全问题提供了专业的解决方式: 同步机制。
 

八、线程安全问题的解决——Synchronized同步机制

Java对于多线程的安全问题提供了专业的解决方式:同步机制。同步机制可以分为同步代码块同步方法。

1、同步代码块:代码如下,同步监视器相当于一把锁,这个锁可以为任意的类对象,若多线程是用实现Runnable接口的方式实现,则考虑使用this充当对象,若多线程是用继承Thread类的方式实现,则考虑使用当前类充当对象。其中需要被同步的代码即为run()方法中的代码。注意:多个线程必须使用同一把锁,即同一个对象。


  
  1. synchronized (同步监视器){
  2. // 需要被同步的代码;
  3. }
  • 同步代码块处理实现Runnable接口方式的线程安全的问题:

  
  1. //自定义一个子类实现Runnable接口
  2. class FirstThread implements Runnable{
  3. //重写run方法
  4. @Override
  5. public void run() {
  6. //遍历输出0——50的偶数
  7. synchronized( this) {
  8. for ( int i = 0; i < 50; i++) {
  9. if(i % 2 == 0){
  10. System.out.println(i);
  11. }
  12. }
  13. }
  14. }
  15. }
  • 同步代码块处理继承Thread类方式的线程安全的问题:

  
  1. //自定义一个子类继承Thread类
  2. class FirstThread extends Thread{
  3. //重写run方法
  4. @Override
  5. public void run() {
  6. synchronized (FirstThread.class){ //
  7. for ( int i = 0; i < 50; i++) {
  8. if(i % 2 == 0){
  9. System.out.println(i);
  10. }
  11. }
  12. }
  13. }
  14. }

 

2、同步方法:synchronized还可以放在方法声明中,表示整个方法为同步方法。同步方法仍然涉及到同步监视器,只是不用我们自己去声明,当为非静态的同步方法时,同步监视器是this;当为静态的同步方法时,同步监视器是当前类。

  • 同步方法处理实现Runnable接口方式的线程安全的问题:

  
  1. //自定义一个子类实现Runnable接口
  2. class FirstThread implements Runnable{
  3. //重写run方法
  4. @Override
  5. public void run() {
  6. show();
  7. }
  8. private synchronized void show(){ //此时同步监视器为:this
  9. for ( int i = 0; i < 50; i++) {
  10. if(i % 2 == 0){
  11. System.out.println(i);
  12. }
  13. }
  14. }
  15. }
  • 同步方法处理继承Thread类方式的线程安全的问题:

   
  1. //自定义一个子类继承Thread类
  2. class FirstThread extends Thread{
  3. //重写run方法
  4. @Override
  5. public void run() {
  6. show();
  7. }
  8. private static synchronized void show(){ //静态方法,此时同步监视器为当前类:FirstThread.class
  9. for ( int i = 0; i < 50; i++) {
  10. if(i % 2 == 0){
  11. System.out.println(i);
  12. }
  13. }
  14. }
  15. }

九、线程安全的单例模式之懒汉式

单例模式:保证类在内存中只能有一个对象。单例模式分为 懒汉式饿汉式
 
  • 懒汉式:默认不会实例化,什么时候用什么时候创建对象。
  • 饿汉式:类加载的时候就实例化,并且创建单例对象。

懒汉式


  
  1. public class Lazy {
  2. private static Lazy lazy;
  3. public static Lazy getInstance(){
  4. //用的时候才去创建
  5. if(lazy == null){
  6. lazy = new Lazy();
  7. }
  8. return lazy;
  9. }
  10. }

饿汉式


  
  1. public class Hungry{
  2. //私有化构造器
  3. private Hungry(){}
  4. //类加载的时候就实例化,并且创建单例对象
  5. private static final Hungry hungry= new Hungry();
  6. public static Hungry getInstance(){
  7. return hungry;
  8. }
  9. }
线程安全问题:
  • 饿汉式线程安全 :在线程还没出现之前就已经实例化了,因此饿汉式线程一定是安全的。
  • 懒汉式线程不安全:因为懒汉式是用的时候才创建,这时可能会发生这种情况:线程A进入getInstance()方法发现没有lazy对象,于是准备创建,而在此时线程B也进入了getInstance()方法,也发现没有lazy对象,于是也创建了一个lazy对象,这样A、B两个线程就创建了两个不同的lazy对象,这就不满足我们的单例模式的要求,因此说线程不安全。

同步机制将懒汉式优化为线程安全的单例模式!!!

优化一:效率稍低,每个线程都需要等着前面的线程释放锁之后才能进去拿着对象出来。


  
  1. public class Lazy {
  2. private static Lazy lazy;
  3. public static synchronized Lazy getInstance(){ //静态方法,此时同步监视器为当前类:Lazy.class
  4. if(lazy == null){
  5. lazy = new Lazy();
  6. }
  7. return lazy;
  8. }
  9. }
优化二:效率高,越靠后的线程越不易等待,前面的线程已经创建好了对象之后,后面的线程只需要在最外层判断一下是否有对象即可,若对象存在的话直接就可以拿着对象走了。
 

   
  1. public class Lazy {
  2. private static Lazy lazy;
  3. public static Lazy getInstance(){
  4. if(lazy == null){
  5. synchronized (Lazy.class) {
  6. if(lazy == null)
  7. lazy = new Lazy();
  8. }
  9. }
  10. return lazy;
  11. }
  12. }

十、死锁问题

死锁: 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
 
死锁的简单示例:如下面的死锁代码样例,线程一拿到a锁后发生阻塞,此时线程二拿到b锁发生阻塞,当两个线程阻塞结束后,线程一需要继续拿到b锁,而此时b锁在线程二手里,同样,线程此时需要继续拿到a锁,但a锁在线程一手上,双方这时都占用着对方需要的同步资源不放弃,发生死锁现象。
 

   
  1. package com.hedong;
  2. /**
  3. * @author hedong
  4. * @version 1.0
  5. * @date 2020/4/8 15:05
  6. */
  7. public class DeadLock {
  8. public static String a = "a";
  9. public static String b = "b";
  10. public static void main(String[] args){
  11. Thread a = new Thread( new MyThread1());
  12. Thread b = new Thread( new MyThread2());
  13. a.start();
  14. b.start();
  15. }
  16. }
  17. class MyThread1 implements Runnable{
  18. @Override
  19. public void run(){
  20. try{
  21. while( true){
  22. synchronized(DeadLock.a){ //同步监视器:a
  23. System.out.println( "线程一,拿到a锁");
  24. Thread.sleep( 3000); //让线程一睡眠,充当阻塞状态
  25. synchronized(DeadLock.b){ //同步监视器:b
  26. System.out.println( "线程一,拿到b锁");
  27. }
  28. }
  29. }
  30. } catch(Exception e){
  31. e.printStackTrace();
  32. }
  33. }
  34. }
  35. class MyThread2 implements Runnable{
  36. @Override
  37. public void run(){
  38. try{
  39. while( true){
  40. synchronized(DeadLock.b){
  41. System.out.println( "线程二,拿到b锁");
  42. Thread.sleep( 3000); //让线程二睡眠,充当阻塞状态
  43. synchronized(DeadLock.a){
  44. System.out.println( "线程二,拿到a锁");
  45. }
  46. }
  47. }
  48. } catch(Exception e){
  49. e.printStackTrace();
  50. }
  51. }
  52. }

运行结果:

 

所以,在程序代码中我们一定要避免死锁的发生!

 

十一、线程安全问题的解决——Lock锁 

 在前面我们使用了Synchronized同步机制来解决线程安全问题,在jdk5.0之后,新增了一种Lock锁用来解决线程安全问题。


  
  1. class FirstThread extends Thread{
  2. //实例化ReentrantLock
  3. private ReentrantLock lock = new ReentrantLock( true);
  4. //重写run方法
  5. @Override
  6. public void run() {
  7. try{
  8. //调用lock方法上锁
  9. lock.lock();
  10. for ( int i = 0; i < 50; i++) {
  11. if(i % 2 == 0){
  12. System.out.println(i);
  13. }
  14. }
  15. } finally {
  16. //调用unlock解锁
  17. lock.unlock();
  18. }
  19. }
  20. }

十二、Lock锁和Synchronized的比较

  • 相同点两者都是同步机制,都能解决线程安全问题。
  • 不同点synchronized在执行完相应的同步代码后会自动释放同步监视器,而Lock锁需要在同步代码之前手动上锁开启同步,在结束之后也要手动解锁结束同步。

十三、wait()、notify()、notifyAll()

  • wait():一旦执行此方法,当前线程将会进入阻塞状态,并释放同步监视器。
  • notify():一旦执行此方法就会唤醒一个被wait()的线程,若有多个被wait()的线程,则将唤醒优先级最高的线程。
  • notifyAll():一旦执行此方法,将会唤醒所有被wait()的线程。
注意:
  • 以上三个方法必须使用在同步代码块或同步方法中。
  • 以上三个方法的调用者必须为同步代码块或同步方法中的同步监视器。

十四、sleep()和wait()的异同

相同点一旦执行两个方法,都能使当前线程进入阻塞状态。

不同点

  • 两个方法声明的位置不同:sleep()是在Thread类中声明,而wait()是在Object类中声明。
  • 两个方法调用的要求不同:sleep()可以在任意需要的地方调用,而wait()只能在同步代码块和同步方法中调用。
  • 关于是否释放同步监视器:当两个方法都在同步代码块或同步方法中调用时,sleep()不会释放同步监视器,而wait()会释放同步监视器。

十五、用实现Callable接口的方式创建多线程

JDK5.0新增了两种线程创建方式:实现Callable接口的方式创建多线程使用线程池我们这里先讲实现Callable接口的方式创建多线程。

步骤:

  1. 定义一个子类实现Callable接口。
  2. 子类中重写Callablecall()方法,注意call方法有返回值。
  3. 在主线程中创建Callable接口实现类的对象。
  4. 将Callable接口实现类的对象作为参数传递给FutureTask类的含参构造器中,创建FutureTask对象。
  5. FutureTask对象作为参数传递给Thread类的含参构造器中,创建线程对象。
  6. 调用线程对象的start()方法。
  7. 如果需要call方法的返回值,可通过FutureTask对象的get()方法获取。

   
  1. package com.hedong;
  2. import java.util.concurrent.Callable;
  3. import java.util.concurrent.ExecutionException;
  4. import java.util.concurrent.FutureTask;
  5. /**
  6. * @author hedong
  7. * @version 1.0
  8. * @date 2020/4/8 16:46
  9. */
  10. class MyThread implements Callable {
  11. //重写call方法,call方法有返回值
  12. @Override
  13. public Object call() throws Exception {
  14. //求1——10的累加和
  15. int sum= 0;
  16. for ( int i = 1; i <= 10; i++) {
  17. sum = sum + i;
  18. }
  19. return sum;
  20. }
  21. }
  22. public class ThreadTest {
  23. public static void main(String[] args) {
  24. //创建callable接口实现类的对象
  25. MyThread myThread = new MyThread();
  26. //将callable接口实现类的对象作为参数传递到FutureTask的含参构造器中,创建FutureTask的对象
  27. FutureTask futureTask= new FutureTask(myThread);
  28. //将FutureTask对象作为参数传递到Thread的含参构造器中,创建线程对象,并调用线程对象的start方法
  29. new Thread(futureTask).start();
  30. try {
  31. //如果需要call方法的返回值,可以通过get方法获取
  32. Object sum = futureTask.get();
  33. System.out.println( "总和为:"+sum);
  34. } catch (InterruptedException e) {
  35. e.printStackTrace();
  36. } catch (ExecutionException e) {
  37. e.printStackTrace();
  38. }
  39. }
  40. }

实现Callable接口的方式看起来很复杂,但为什么确认为实现Callable接口的方式比实现Runnable接口的方式更好呢?

原因:
  • call()可以有返回值。
  • call()可以抛出异常,可以让外面的操作获取异常信息。
  • callable支持泛型

十六、使用线程池创建多线程

线程池的概念: 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。
 

步骤:

  •  自定义一个子类实现Runable接口或Callable接口。
  • 创建一个指定线程数量的线程池
  • 执行指定线程操作,需要提供Runable接口或Callable接口实现类对象
  • 关闭线程池。

   
  1. package com.hedong;
  2. import java.util.concurrent.Executor;
  3. import java.util.concurrent.ExecutorService;
  4. import java.util.concurrent.Executors;
  5. /**
  6. * @author hedong
  7. * @version 1.0
  8. * @date 2020/4/8 21:21
  9. */
  10. class FirstThread implements Runnable{
  11. @Override
  12. public void run() {
  13. //遍历输出0——50的偶数
  14. for ( int i = 0; i < 50; i++) {
  15. if(i % 2 == 0){
  16. System.out.println(i);
  17. }
  18. }
  19. }
  20. }
  21. public class ThreadPool {
  22. public static void main(String[] args) {
  23. //提供指定线程数量的线程池
  24. ExecutorService service= Executors.newFixedThreadPool( 10);
  25. //执行指定的线程操作,需要提供Runnable接口或Callable接口实现类的对象
  26. service.execute( new FirstThread()); //适合用于提供的是Runnable接口实现类的对象
  27. //service.submit();//适合用于提供的是Callable接口实现类的对象,因为可以获取返回值
  28. //关闭线程池
  29. service.shutdown();
  30. }
  31. }

线程池的好处:

  • 提高响应速度(减少了创建新线程的时间)。
  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)。
  • 便于线程管理。
 

转载:https://blog.csdn.net/h2503652646/article/details/105372630
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场