多线程相关知识总结

Posted reecelin

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程相关知识总结相关的知识,希望对你有一定的参考价值。

1. 进程与线程的不同

  • 进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。(进程是资源分配的最小单位)。多进程是指操作系统同时运行多个任务(程序)。

  • 线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位),又称为轻量级进程。多进程是指在同一程序中有多个顺序流在执行。

2. 实现多线程的方法

2.1 继承Thread类,并重写其run()方法

public class ThreadTest extends Thread{
   private int num;
   
   public ThreadTest(String name,int num){
       super(name);
       this.num=num;
  }
   
   @Override
   public void run(){
              System.out.println(Thread.currentThread().getName());
       for(int i=0;i<49;i++){
           num++;
           System.out.print(" "+num);
      }
  }
   
   
   public static void main(String[] args){
       ThreadTest tt=new ThreadTest("threadTest",1);
       tt.start();
  }
}

2.2 实现Runnable接口,并使用Thread类进行包装

public class RunnableTest implements Runnable{
   private int num;
   
   public RunnableTest(int num){
       this.num=num;
  }
   
   @Override
   public void run(){
       Sytem.out.println(Thread.currentThread().getName());
       for(int i=0;i<49;i++){
           num++;
           System.out.print(num);
      }
  }
   
   public static void main(String[] args){
       RunnableTest rt=new RunnableTest(1);
       Thread t=new Thread(rt);
       t.start();
  }
}

2.3 实现Callable<>接口,并使用FutureTask类和Thread类进行包装

public class CallableTest implements Callable<Integer>{
   
   private int num;
   
  public CallableTest(int num){
       this.num=num;
  }


   @Override
   public Integer call() throws Exception{
       System.out.println(Thread.currentThread().getName());
       for(int i=0;i<49;i++){
           num++;
      }
       return num;
  }


   public static void main(String[] args) throws ExecutionException, InterruptedException {
       CallableTest ct=new CallableTest(1);
       FutureTask task=new FutureTask(ct);
       Thread t=new Thread(task);
       t.start();
       System.out.println(task.get());
  }
}

2.4 使用线程池ExecutorService来生成和管理线程

public class ThreadPoolTest{
   
   public static void main(String[] args){
    ExecutorService cachedThreadPool=Executors.newCachedThreadPool();
      ExecutorService fixedThreadPool=Executors.newFixedThreadPool(10);
      ExecutorService singledThreadPool=Executors.newSingleThreadExecutor();
      ExecutorService scheduledThreadPool =Executors.newScheduledThreadPool(10);
       cachedThreadPool.execute(()->{
          for (int i = 0; i <50 ; i++) {
              System.out.println("i:"+i);
          }
      });
        cachedThreadPool.shutdown();
  }
}

 

2.5 Thread类,Runnable接口中的run()方法与Callable中的call()方法的不同

  • run()方法没有返回值,不能抛出异常,只能在run()方法内部对异常进行处理;call()方法有返回值,作为任务的执行结果,且可以直接抛出异常;

  • Runnable可以作为Thread构造器的参数,可通过开启新的线程来执行,也可通过线程池来执行;Callable只能通过线程池执行。

2.6 四种线程池的比较

四种线程池都是由Executors类的静态方法生成的。观察源码可知,这四种线程池最终都是由ThreadPoolExecutor类构造而成。区别在于构造器的参数不同。我们来看下ThreadPoolExecutor的参数列表:

  public ThreadPoolExecutor(int corePoolSize,
                             int maximumPoolSize,
                             long keepAliveTime,
                             TimeUnit unit,
                             BlockingQueue<Runnable> workQueue,
                             RejectedExecutionHandler handler) {
       this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
            Executors.defaultThreadFactory(), handler);
  }

ThreadPoolExecutor各参数含义:

  • corePoolSize:核心线程数量,常驻在线程池中的线程,即使它们是空闲的,也不会销毁,除非设置allowCoreThreadTimeOut的值。

  • maximumPoolSize:线程池最大线程数量

  • keepAliveTime:超过核心数量的额外线程也就是非核心线程,在空闲指定的最大时间后被销毁。(假设时间为5s,核心线程数为2,当前线程为4,则超过核心线程数的其余两个线程在空闲5秒后会被销毁。)

  • unit:时间单位

  • workQueue:等待队列

  • threadFactory:生成线程的工厂

  • handler:当等待队列容量满以及线程池数量达到最大时,如何处理新的任务:

    • AbortPolicy(默认):直接抛出异常

    • CallerRunsPolicy:交给调用者所在线程执行。(假设当前调用者线程是Main,那么就交给Main处理)

    • DiscardOldestPolicy:丢弃最久未处理的任务,再执行当前任务。(最久未处理的,在队列中其实就是队列头节点,查看源码的确调用是poll()方法)

    • DiscardPolicy:丢掉该任务,并且不抛异常。

2.6.1 线程池的工作机制:

当持续往线程池添加任务, 当前线程数量小于核心线程数量的时候,新增线程。 当前线程数量达到核心线程数量的时候,将任务放入等待队列。 当等待队列满的时候,继续创建新线程。 当线程池数量达到最大并且等待队列也满的时候,采取拒绝服务策略。

接下来我们就根据参数来分析不同的线程池:

2.6.1.1 FixedThreadPool
 public static ExecutorService newFixedThreadPool(int nThreads) {
       return new ThreadPoolExecutor(nThreads, nThreads,
                                     0L, TimeUnit.MILLISECONDS,
                                     new LinkedBlockingQueue<Runnable>());
  }

我们可以看到corePoolSize核心线程数量和maximumPoolSize最大线程数量是一致的,并且keepAliveTime为0。workQueue是LinkedBlockingQueue,这是一个链表阻塞队列。可以得出结论:该线程池是一个固定数量的线程池,并且有一个无界的等待队列。我们可以推导出该线程池适合处理任务量平稳的场景。例如平均一秒接收10个任务,接收任务量曲线不会很陡峭。

适合场景:适合少量的大任务(大任务处理慢,如果线程数量多的话,反而在切换线程上下文时损耗,所以控制线程在一定的数量)。

2.6.1.2 CachedThreadPool
public static ExecutorService newCachedThreadPool() {
       return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                     60L, TimeUnit.SECONDS,
                                     new SynchronousQueue<Runnable>());
  }

我们可以看到corePoolSize核心线程池为0,代表该线程没有核心线程池,意味着线程都是可被回收销毁的,线程池中有时会是空的。并且maximumPoolSize是int最大值,相当于代表该线程池可以无限创建线程。keepAliveTime为60,代表空闲60秒回收线程。workQueue是SynchronousQueue,该同步队列是一个没有容量队列,即一个任务到来后,要等待线程来消费,才能再继续添加任务。我们推导出该线程池适合处理平时没什么任务量,但有时任务量瞬间剧增的场景。

适合场景:大量的小任务(每个任务处理快,不会频繁出现线程处理一半时,切换其他线程)。

2.6.1.3 ScheduledThreadPool
public ScheduledThreadPoolExecutor(int corePoolSize) {
       super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
             new DelayedWorkQueue());
  }

我们可以看到该线程池参数最大的区别在于workQueue是DelayedWorkQueue。该队列是一个按延迟时间从小到大排序的堆。并且当队列头节点的延迟时间小于0的时候返回该节点。所以该线程池可以指定一个时间进行定时任务。也可以通过添加任务时递增延迟时间,来进行周期任务。

适合场景:定时任务或者周期任务。

2.6.1.4 SingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
       return new FinalizableDelegatedExecutorService
          (new ThreadPoolExecutor(1, 1,
                                   0L, TimeUnit.MILLISECONDS,
                                   new LinkedBlockingQueue<Runnable>()));
  }

我们可以看到该线程池的corePoolSize核心线程数量和maximumPoolSize最大线程数量都是1,代表该线程有且只有一个固定的线程,既然是单线程,所以该线程池实现的是串行操作,没有并发效果。workQueue是LinkedBlockingQueue,这是一个链表阻塞队列。所以该线程池适合执行串行执行队列中的任务。

适合场景:按顺序串行处理的任务。

keepAliveTime为0代表的含义? 是立即回收线程还是永不回收呢?

keepAliveTime参数注释明确指明只对非核心线程有用。 我们可以从ScheduledThreadPool的源码中推测,如果0代表是永不回收的话,那么ScheduledThreadPool一旦创建出非核心线程的话就不会回收了?这样是很不合理的。所以0代表立即回收。

public ScheduledThreadPoolExecutor(int corePoolSize) {
       super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
             new DelayedWorkQueue());
  }

3. 线程的几种状态

3.1 线程状态

线程共有六种状态,分别为:

  • 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。

  1. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。 线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。

  2. 阻塞(BLOCKED):表示线程阻塞于锁。

  3. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

  4. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。

  • 终止(TERMINATED):表示该线程已经执行完毕。

状态图如下所示:

技术图片

初始状态 实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。

就绪状态 就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。 调用线程的start()方法,此线程进入就绪状态。 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。 当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。 锁池里的线程拿到对象锁后,进入就绪状态。 运行中状态 线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。

阻塞状态 阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。

等待 处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。

超时等待 处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。

终止状态 当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。 在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

3.2 等待队列

  • 调用obj的wait(), notify()方法前,必须获得obj锁,也就是必须写在synchronized(obj) 代码段内。

  • 与等待队列相关的步骤和图

技术图片

  • 线程1获取对象A的锁,正在使用对象A。

  • 线程1调用对象A的wait()方法。

  • 线程1释放对象A的锁,并马上进入等待队列。

  • 锁池里面的对象争抢对象A的锁。

  • 线程5获得对象A的锁,进入synchronized块,使用对象A。

  • 线程5调用对象A的notifyAll()方法,唤醒所有线程,所有线程进入同步队列。

  • 若线程5调用对象A的notify()方法,则唤醒一个线程,不知道会唤醒谁,被唤醒的那个线程进入同步队列

  • notifyAll()方法所在synchronized结束,线程5释放对象A的锁。

  • 同步队列的线程争抢对象锁,但线程1什么时候能抢到就不知道了。

3.3 同步队列状态

当前线程想调用对象A的同步方法时,发现对象A的锁被别的线程占有,此时当前线程进入同步队列。简言之,同步队列里面放的都是想争夺对象锁的线程。 当一个线程1被另外一个线程2唤醒时,1线程进入同步队列,去争夺对象锁。 同步队列是在同步的环境下才有的概念,一个对象对应一个同步队列。 线程等待时间到了或被notify/notifyAll唤醒后,会进入同步队列竞争锁,如果获得锁,进入RUNNABLE状态,否则进入BLOCKED状态等待获取锁。

3.4 常用方法

Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,但不释放对象锁,millis后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式。 Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间。 thread.join()/thread.join(long millis),当前线程里调用其它线程t的join方法,当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁。线程t执行完毕或者millis时间到,当前线程一般情况下进入RUNNABLE状态,也有可能进入BLOCKED状态(因为join是基于wait实现的)。 obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout) timeout时间到自动唤醒。 obj.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。 LockSupport.park()/LockSupport.parkNanos(long nanos),LockSupport.parkUntil(long deadlines), 当前线程进入WAITING/TIMED_WAITING状态。对比wait方法,不需要获得锁就可以让线程进入WAITING/TIMED_WAITING状态,需要通过LockSupport.unpark(Thread thread)唤醒。

4.多线程同步

当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常。举个例子,如果一个银行账户同时被两个线程操作,一个取100块,一个存钱100块。假设账户原本有0块,如果取钱线程和存钱线程同时发生,会出现什么结果呢?取钱不成功,账户余额是100.取钱成功了,账户余额是0.那到底是哪个呢?很难说清楚。因此多线程同步就是要解决这个问题。

4.1 不同步时的代码

bank.java

1. public class Bank {  
2.  
3. •    private int count =0;//账户余额  
4. •      
5. •    //存钱  
6. •    public  void addMoney(int money){  
7. •        count +=money;  
8. •        System.out.println(System.currentTimeMillis()+"存进:"+money);  
9. •   }  
10. •      
11. •    //取钱  
12. •    public  void subMoney(int money){  
13. •        if(count-money < 0){  
14. •            System.out.println("余额不足");  
15. •            return;  
16. •       }  
17. •        count -=money;  
18. •        System.out.println(+System.currentTimeMillis()+"取出:"+money);  
19. •   }  
20. •      
21. •    //查询  
22. •    public void lookMoney(){  
23. •        System.out.println("账户余额:"+count);  
24. •   }  
25. }  

SyncThreadTest.java

1. public class SyncThreadTest {  
2.  
3. •    public static void main(String args[]){  
4. •        final Bank bank=new Bank();  
5. •          
6. •        Thread tadd=new Thread(new Runnable() {  
7. •              
8. •            @Override  
9. •            public void run() {  
10. •                // TODO Auto-generated method stub  
11. •                while(true){  
12. •                    try {  
13. •                        Thread.sleep(1000);  
14. •                   } catch (InterruptedException e) {  
15. •                        // TODO Auto-generated catch block  
16. •                        e.printStackTrace();  
17. •                   }  
18. •                    bank.addMoney(100);  
19. •                    bank.lookMoney();  
20. •                    System.out.println(" ");  
21. •                      
22. •               }  
23. •           }  
24. •       });  
25. •          
26. •        Thread tsub = new Thread(new Runnable() {  
27. •              
28. •            @Override  
29. •            public void run() {  
30. •                // TODO Auto-generated method stub  
31. •                while(true){  
32. •                    bank.subMoney(100);  
33. •                    bank.lookMoney();  
34. •                    System.out.println(" ");  
35. •                    try {  
36. •                        Thread.sleep(1000);  
37. •                   } catch (InterruptedException e) {  
38. •                        // TODO Auto-generated catch block  
39. •                        e.printStackTrace();  
40. •                   }    
41. •               }  
42. •           }  
43. •       });  
44. •        tsub.start();  
45. •          
46. •        tadd.start();  
47. •   }  
48. •      
49. •      
50.  
51. }  
控制台输出:

余额不足  
账户余额:0

余额不足  
账户余额:100

1441790503354存进:100  
账户余额:100    

1441790504354存进:100  
账户余额:100  

1441790504354取出:100  
账户余额:100  

1441790505355存进:100  
账户余额:100

1441790505355取出:100  
账户余额:100  

4.2 使用同步时的代码

4.2.1 同步方法:

即有synchronized关键字修饰的方法。 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

修改后的Bank.java

1. public class Bank {  
2.  
3. •    private int count =0;//账户余额  
4. •      
5. •    //存钱  
6. •    public  synchronized void addMoney(int money){  
7. •        count +=money;  
8. •        System.out.println(System.currentTimeMillis()+"存进:"+money);  
9. •   }  
10. •      
11. •    //取钱  
12. •    public  synchronized void subMoney(int money){  
13. •        if(count-money < 0){  
14. •            System.out.println("余额不足");  
15. •            return;  
16. •       }  
17. •        count -=money;  
18. •        System.out.println(+System.currentTimeMillis()+"取出:"+money);  
19. •   }  
20. •      
21. •    //查询  
22. •    public void lookMoney(){  
23. •        System.out.println("账户余额:"+count);  
24. •   }  
25. }  

再次运行SynchronizedTest

控制台输出:
余额不足  
账户余额:0  
 
余额不足  
账户余额:0  

1441790837380存进:100  
账户余额:100  
 
1441790838380取出:100  
账户余额:0  
1441790838380存进:100  
账户余额:100  
 
1441790839381取出:100  
账户余额:0  

注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类

4.2.2 同步代码块

即有synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步

Bank.java

1. public class Bank {  
2.  
3. •    private int count =0;//账户余额  
4. •      
5. •    //存钱  
6. •    public   void addMoney(int money){  
7. •          
8. •        synchronized (this) {  
9. •            count +=money;  
10. •       }  
11. •        System.out.println(System.currentTimeMillis()+"存进:"+money);  
12. •   }  
13. •      
14. •    //取钱  
15. •    public   void subMoney(int money){  
16. •          
17. •        synchronized (this) {  
18. •            if(count-money < 0){  
19. •                System.out.println("余额不足");  
20. •                return;  
21. •           }  
22. •            count -=money;  
23. •       }  
24. •        System.out.println(+System.currentTimeMillis()+"取出:"+money);  
25. •   }  
26. •      
27. •    //查询  
28. •    public void lookMoney(){  
29. •        System.out.println("账户余额:"+count);  
30. •   }  
31. }  
控制台输出如下:
余额不足  
账户余额:0  
 
1441791806699存进:100  
账户余额:100  
 
1441791806700取出:100  
账户余额:0  
 
 
1441791807699存进:100  
账户余额:100  

同步代码块和同步方法的效果相同。

注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

4.2.3 使用特殊域变量(volatile)实现线程同步

* volatile关键字为域变量的访问提供了一种免锁机制 ;
* 使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新 ;
* 因此每次使用该域就要重新计算,而不是使用寄存器中的值 ;
* volatile不会提供任何原子操作,它也不能用来修饰final类型的变量;

Bank.java代码如下:

1. public class Bank {  
2.  
3. •    private volatile int count = 0; // 账户余额  
4.  
5. •    // 存钱  
6. •    public void addMoney(int money) {  
7.  
8. •        count += money;  
9. •        System.out.println(System.currentTimeMillis() + "存进:" + money);  
10. •   }  
11.  
12. •    // 取钱  
13. •    public void subMoney(int money) {  
14.  
15. •        if (count - money < 0) {  
16. •            System.out.println("余额不足");  
17. •            return;  
18. •       }  
19. •        count -= money;  
20. •        System.out.println(+System.currentTimeMillis() + "取出:" + money);  
21. •   }  
22.  
23. •    // 查询  
24. •    public void lookMoney() {  
25. •        System.out.println("账户余额:" + count);  
26. •   }  
27. }  
控制台显示:
余额不足  
账户余额:0  
 
余额不足  
账户余额:100  
 
1441792010959存进:100  
账户余额:100  

1441792011960取出:100  
账户余额:0  
 
1441792011961存进:100  
账户余额:100  

又乱了。这是为什么呢?这是因为volatile不能保证原子操作导致的,因此volatile不能代替synchronized。此外volatile会组织编译器对代码优化,因此能不使用它就不适用它吧。

4.2.4 使用重入锁实现线程同步

在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。 ReenreantLock类的常用方法有: ReentrantLock() : 创建一个ReentrantLock实例 lock() : 获得锁unlock() : 释放锁

注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用 Bank.java

1. public class Bank {  
2.  
3. •    private  int count = 0;// 账户余额  
4. •      
5. •    //需要声明这个锁  
6. •    private Lock lock = new ReentrantLock();  
7.  
8. •    // 存钱  
9. •    public void addMoney(int money) {  
10. •        lock.lock();//上锁  
11. •        try{  
12. •        count += money;  
13. •        System.out.println(System.currentTimeMillis() + "存进:" + money);  
14. •          
15. •       }finally{  
16. •            lock.unlock();//解锁  
17. •       }  
18. •   }  
19.  
20. •    // 取钱  
21. •    public void subMoney(int money) {  
22. •        lock.lock();  
23. •        try{  
24. •              
25. •        if (count - money < 0) {  
26. •            System.out.println("余额不足");  
27. •            return;  
28. •       }  
29. •        count -= money;  
30. •        System.out.println(+System.currentTimeMillis() + "取出:" + money);  
31. •       }finally{  
32. •            lock.unlock();  
33. •       }  
34. •   }  
35. // 查询  
36. •    public void lookMoney() {  
37. •        System.out.println("账户余额:" + count);  
38. •   }  
39. }  

 

控制台显示:
余额不足  
账户余额:0  
 
余额不足  
账户余额:0  
 
1441792891934存进:100  
账户余额:100  
 
1441792892935存进:100  
账户余额:200  

1441792892954取出:100  
账户余额:100  

效果和前两种方法差不多。

如果synchronized关键字能满足用户的需求,就用synchronized,因为它能简化代码 。如果需要更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁

4.2.4 使用局部变量实现线程同步

Bank.java

1. public class Bank {  
2.  
3. •    private static ThreadLocal<Integer> count = new ThreadLocal<Integer>(){  
4.  
5. •        @Override  
6. •        protected Integer initialValue() {  
7. •            // TODO Auto-generated method stub  
8. •            return 0;  
9. •       }  
10. •          
11. •   };  
12. •      
13.  
14. •    // 存钱  
15. •    public void addMoney(int money) {  
16. •        count.set(count.get()+money);  
17. •        System.out.println(System.currentTimeMillis() + "存进:" + money);  
18. •          
19. •   }  
20.  
21. •    // 取钱  
22. •    public void subMoney(int money) {  
23. •        if (count.get() - money < 0) {  
24. •            System.out.println("余额不足");  
25. •            return;  
26. •       }  
27. •        count.set(count.get()- money);  
28. •        System.out.println(+System.currentTimeMillis() + "取出:" + money);  
29. •   }  
30.  
31. •    // 查询  
32. •    public void lookMoney() {  
33. •        System.out.println("账户余额:" + count.get());  
34. •   }  
35. }  
控制台输出:
余额不足  
账户余额:0  
 
余额不足  
账户余额:0  

1441794247939存进:100  
账户余额:100  
 
余额不足  
1441794248940存进:100  
账户余额:0  

账户余额:200  

余额不足  
账户余额:0  

1441794249941存进:100  
账户余额:300  

看了运行效果,只让存,不让取啊?看看ThreadLocal的原理:

如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。现在明白了吧,原来每个线程运行的都是一个副本,也就是说存钱和取钱是两个账户,知识名字相同而已。所以就会发生上面的效果。

ThreadLocal与同步机制

  • ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题

  • 前者采用以"空间换时间"的方法,后者采用以"时间换空间"的方式

5. ThreadLocal

ThreadLocal翻译成中文比较准确的叫法应该是:线程局部变量。

  这个玩意有什么用处,或者说为什么要有这么一个东东?先解释一下,在并发编程的时候,成员变量如果不做任何处理其实是线程不安全的,各个线程都在操作同一个变量,显然是不行的,并且我们也知道volatile这个关键字也是不能保证线程安全的。那么在有一种情况之下,我们需要满足这样一个条件:变量是同一个,但是每个线程都使用同一个初始值,也就是使用同一个变量的一个新的副本。这种情况之下ThreadLocal就非常使用,比如说DAO的数据库连接,我们知道DAO是单例的,那么他的属性Connection就不是一个线程安全的变量。而我们每个线程都需要使用他,并且各自使用各自的。这种情况,ThreadLocal就比较好的解决了这个问题。

  首先定义一个ThreadLocal:

public final class ConnectionUtil {

   private ConnectionUtil() {}

   private static final ThreadLocal<Connection> conn = new ThreadLocal<>();

   public static Connection getConn() {
       Connection con = conn.get();
       if (con == null) {
           try {
               Class.forName("com.mysql.jdbc.Driver");
               con = DriverManager.getConnection("url", "userName", "password");
               conn.set(con);
          } catch (ClassNotFoundException | SQLException e) {
               // ...
          }
      }
       return con;
  }

}

  这样子,都是用同一个连接,但是每个连接都是新的,是同一个连接的副本。

5.1 什么是线程封闭

当访问共享变量时,往往需要加锁来保证数据同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程中访问数据,就不需要同步了。这种技术称为线程封闭。在Java语言中,提供了一些类库和机制来维护线程的封闭性,例如局部变量和ThreadLocal类,本文主要深入讲解如何使用ThreadLocal类来保证线程封闭。

理解ThreadLocal类 ThreadLocal类能使线程中的某个值与保存值的对象关联起来,它提供了get、set方法,这些方法为每个使用该变量的线程保存一份独立的副本,因此get总是set当前线程的set最新值。

public class Test1 {

   ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
   ThreadLocal<String> stringLocal = new ThreadLocal<String>();


   public void set() {
       longLocal.set(Thread.currentThread().getId());
       stringLocal.set(Thread.currentThread().getName());
  }

   public long getLong() {
       return longLocal.get();
  }

   public String getString() {
       return stringLocal.get();
  }
   public static void main(String[] args) throws InterruptedException {
       final Test1 test = new Test1();


       test.set();
       System.out.println(test.getLong());
       System.out.println(test.getString());


       Thread thread1 = new Thread(() -> {
           test.set();
           System.out.println(test.getLong());
           System.out.println(test.getString());
      });
       thread1.start();
       thread1.join();

       System.out.println(test.getLong());
       System.out.println(test.getString());
  }
}
控制台输出:
1
main
10
Thread-0
1
main

可以看出在mian线程和thread1线程确实都保存着各自的副本,它们的副本各自不干扰。

5.2 ThreadLocal源码解析

来从源码的角度来解析ThreadLocal这个类,这个类存放在java.lang包,这个类有很多方法。

技术图片

它内部又个ThreadLocalMap类,主要有set()、get()、setInitialValue 等方法。

首先来看下set方法,获取当前Thread的 map,如果不存在则新建一个并设置值,如果存在设置值,源码如下:

public void set(T value) {
       Thread t = Thread.currentThread();
       ThreadLocalMap map = getMap(t);
       if (map != null)
           map.set(this, value);
       else
           createMap(t, value);
  }

跟踪createMap,可以发现它根据Thread创建来一个ThreadLocalMap。

 void createMap(Thread t, T firstValue) {
       t.threadLocals = new ThreadLocalMap(this, firstValue);
  }

t.threadLocals为当前线程的一个变量,也就是ThreadLocal的数据都是存放在当前线程的threadLocals变量里面的,由此可见用ThreadLocal存放的数据是线程安全的。因为它对于不同的线程来,使用ThreadLocal的set方法都会根据线程判断该线程是否存在它的threadLocals成员变量,如果没有就建一个,有的话就存下数据。

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap为ThreadLocal的一个内部类,源码如下:

static class ThreadLocalMap {

       static class Entry extends WeakReference<ThreadLocal<?>> {
           /** The value associated with this ThreadLocal. */
           Object value;

           Entry(ThreadLocal<?> k, Object v) {
               super(k);
               value = v;
          }
      }

可以看到ThreadLocalMap的Entry继承了WeakReference,并且使用ThreadLocal作为键值。

在使用ThreadLocal的get方法之前一定要先set,要不然会报空指针异常。还有一种方式就是在初始化的时候调用initialValue()方法赋值。改造下之前的例子,代码如下:

public class Test2 {

   ThreadLocal<Long> longLocal = new ThreadLocal<Long>(){

       @Override
       protected Long initialValue() {
           return Thread.currentThread().getId();
      }
  };
   ThreadLocal<String> stringLocal = new ThreadLocal<String>(){
       @Override
       protected String initialValue() {
           return Thread.currentThread().getName();
      }
  };

   public long getLong() {
       return longLocal.get();
  }

   public String getString() {
       return stringLocal.get();
  }

   public static void main(String[] args) throws InterruptedException {
       final Test2 test = new Test2();



       System.out.println(test.getLong());
       System.out.println(test.getString());


       Thread thread1 = new Thread(() -> {
         
           System.out.println(test.getLong());
           System.out.println(test.getString());
      });
       thread1.start();
       thread1.join();

       System.out.println(test.getLong());
       System.out.println(test.getString());
  }
}
控制台输出:
1
main
10
Thread-0
1
main

5.3 ThreadLocal常用的使用场景

通常讲JDBC连接保存在ThreadLocal对象中,每个对象都有属于自己的连接,代码如下:

private static ThreadLocal<Connection> connectionHolder
= new ThreadLocal<Connection>() {
   public Connection initialValue() {
      return DriverManager.getConnection(DB_URL);
  }
};

public static Connection getConnection() {
   return connectionHolder.get();
}

以上是关于多线程相关知识总结的主要内容,如果未能解决你的问题,请参考以下文章

Android多线程相关知识总结——源码分析

知识储备—01-进程,线程,多线程相关总结

JAVA多线程高并发面试题总结

并发与多线程相关知识点梳理

并发与多线程相关知识点梳理

并发与多线程相关知识点梳理