线程安全性

什么是线程安全性

当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么称这个类是线程安全的。

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何格外的同步或协同,这个类都能表现出正确的行为,那么就成这个类是线程安全的。

注:无状态的对象一定是线程安全的
什么是有状态什么是无状态?
如果一个类的同一个实例被多个线程共享并不会使这些线程存储共享的状态,那么该类的实例就称为无状态对象(Stateless Object). 反之如果一个类的实例被多个线程共享会使这些线程存在共享状态,那么 该类的实例称为有状态对象. 实际上无状态对象就是不包含任何实例变量也不包含任何静态变量的对象。

原子性

java语言中默认简单的写操作时是保证原子性的,eg:int i= 0;但是num++;不是简单的赋值操作。是分为三步“读取-修改-写入”。每一步依赖上一步的状态,每两步中的间断时刻会被别的线程趁虚而入,导致状态的不准确性。

竞态条件

当某个结果的正确性取决于多个线程的交替执行时序时,那么这个时候会发生竞态条件。常见的操作类型就是“先检查后执行”,检查得到的结果在执行的时候不一定还是有效的。

  1. //这种情况在线程交替执行的时候会大概率出现预计结果值不对的情况
  2. private Integer a = 1
  3. if (conditonA) {
  4. a += 1;
  5. }

复合操作

在num++;这个操作的时候,读,修改,写这三步,每一步都是原子性操作,当这三个放在一起的时候为复合操作,此时并不保证该操作的原子性。

多个原子性的操作组合在一起的时候,该操作不能保证其原子性。那么就要使用juc相关的类进行操作或者进行加锁操作

特例

32位虚拟机中long/double类型变量的简单赋值的写操作,不是原子性的。long和double是64位的,在32位中需要拆分成两个32位来做,当两个线程对变量做赋值操作时,有可能A线程对高32位做了修改,B线程对低32位做了修改。再从2进制转换成10进制或16进制的时候就会有问题。
解决方案:对变量设置volatile关键字

加锁机制

内置锁

也就是synchronized关键字支持原子性:同步代码块
1.同步代码块的锁就是方法调用所在的对象。静态的synchronized方法以Class对象作为锁。
2.每个java对象都可以用作一个实现同步的锁,被称为内置锁或者监视器锁。
3.该锁相当于一个互斥体,也就是说最多只有一个线程能持有这种锁,其他线程继续等待直到上个线程释放锁。
4.该关键字会自动加锁自动释放锁。

重入

当一个线程尝试获取自己持有的锁,那么这个请求就会成功。避免了线程调用子类重写父类中的同步方法时产生死锁的问题。“重入”意味着获取锁的操作的粒度是“线程”,而不是“调用”。遇见下边的情况如果不可重入,那么这种情况就会发生死锁。

  1. public void synchronized method1() {
  2. // ...
  3. method2();
  4. // ...
  5. }
  6. public void synchronized method2() {
  7. // ...
  8. }

线程基础

线程创建方式

线程的创建最终实现都是实现Runnable接口或是继承Thread类,任何封装都是根据这两个来做的。
在我们实现Runnable接口,继承Thread的时候同样也要实现或者重写run方法。

  1. private Runnable target;
  2. @Override
  3. public void run() {
  4. if (target != null) {
  5. target.run();
  6. }
  7. }

但实际上创建线程只有一种方式就是构造Thread类,但是实现线程“运行内容”是存在两种方式,就是实现Runnable接口或者继承Thread类。
为什么实现Runnable要比继承Thread类要好?
1.接口与类相比是很轻的也是扩展性较好的一种方式,java中一个类是单继承但是可以有多个接口,因此实现Runnable接口要更好
2.在某种情况下可以减少开销,当一个类本身所执行的方法开销较小,实现Runnable后就可以将这个任务交给线程池来做,减少了线程创建销毁带来的开销。

如何正确停止线程

正常情况下,线程一般不会去手动停止而是允许线程运行到程序结束让其自然停止。但是仍然会出现突然停止的操作。
在java中正确停止线程的方式是使用interrupt方法,这个方法仅仅是通知线程它应该停止了但不会主动去停止这个线程。停止的操作权是交给线程本身来做。这样的好处是各个程序见能够相互通知、相互协作的管理线程,如果不管别的线程在做什么的话就会产生安全问题,interrupt为线程流出来把某些工作做完的时间,任务完成后自动停止线程。并且在线程使用sleep或者wait阻塞时可以感知interrupt的操作。

不要使用stop,suspend,resume方法或volatile标记位停止线程

stop()方法直接去停止线程不会等待线程是否完成当前任务,出现数据不完整的问题
suspend,resume方法在调用的时候不会释放锁,如果他们需要的都是同一把锁那么就会产生死锁问题
volatile做标记时在生产者消费者使用阻塞队列的时候不会正常去运行,如果生产者放入数据进入阻塞队列时阻塞了那么是无法进入下一次循环判断的,这时标记位就是无效的

  1. class Producer implements Runnable {
  2. public volatile boolean canceled = false;
  3. BlockingQueue storage;
  4. public Producer(BlockingQueue storage) {
  5. this.storage = storage;
  6. }
  7. @Override
  8. public void run() {
  9. int num = 0;
  10. try {
  11. while (num <= 100000 && !canceled) {
  12. if (num % 50 == 0) {
  13. storage.put(num);
  14. System.out.println(num + "是50的倍数,被放到仓库中了。");
  15. }
  16. num++;
  17. }
  18. } catch (InterruptedException e) {
  19. e.printStackTrace();
  20. } finally {
  21. System.out.println("生产者结束运行");
  22. }
  23. }
  24. }

wait/notify/notifyAll

wait必须在同步块中使用

“wait method should always be used in a loop:This method should only be called by a thread that is the owner of this object’s monitor.”
这个方法应该被使用在一个循环中,并且当这个方法只能被持有对象监视器的线程所调用。持有对象监视器那么就是说这里持有锁了

  1. synchronized (obj) {
  2. while (condition does not hold)
  3. obj.wait();
  4. ... // Perform action appropriate to condition
  5. }

反例:同一类中使用两个线程去执行take和give方法,存在一种情况就是当while刚好判断完时,此时数据也添加完了wait还没执行,notify就已经执行了那么执行take方法的线程就一直阻塞了。while这种判断也不是原子性的就需要靠加锁保证同步。

  1. class BlockingQueue {
  2. Queue<String> buffer = new LinkedList<String>();
  3. public void give(String data) {
  4. buffer.add(data);
  5. notify(); // Since someone may be waiting in take
  6. }
  7. public String take() throws InterruptedException {
  8. while (buffer.isEmpty()) {
  9. wait();
  10. }
  11. return buffer.remove();
  12. }
  13. }

wait/notify/notifyAll定义在Object中,sleep定义在Thread中

wait/notify/notifyAll都是锁级别的操作,每个对象都有一把监视器锁,这个锁是在对象头上的,Object又是所有类的父类那么定义在Object中就比较合适,如果定义在Thread类中就有许多的局限性,例如无法让一个线程持有多把锁,而且线程是获取锁这个操作是对象级别而不是线程级别。

wait/notify与Sleep区别

1.wait 方法必须在 synchronized 保护的代码中使用,而 sleep 方法并没有这个要求。
2.在同步代码中执行 sleep 方法时,并不会释放 monitor 锁,但执行 wait 方法时会主动释放 monitor 锁。
3.sleep 方法中会要求必须定义一个时间,时间到期后会主动恢复,而对于没有参数的 wait 方法而言,意味着永久等待,直到被中断或被唤醒才能恢复,它并不会主动恢复。
4.wait/notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。

ThreadLocal

线程本地化存储的策略

分析一下各种变量都怎么存储的
image.png
上面这些变量都是属于方法中,每个方法都给封装成一个栈帧放在线程栈中。除了new Object()这种引用类型的真实值不会在局部变量表中,表中只存在object对象的引用和其余变量。
此时由于new Object()在堆中其实是一个共享变量,因此就不是线程安全的。
image.png
为了避免线程安全问题,我们不希望把Object object变量共享,我们可以让每个线程都有一个Object object对象
image.png
这样的话就需要对每个线程都创建这个Object对象,这样就没有共享了,线程就安全了。这样的通用性就不是很好如果可以引入一个类似代理对象,把每个线程持有的Object对象的细节隐藏掉就可以。
image.png
思路是将线程和对应的对象放在MyThreadLocal,这样会有一些问题。只要MyThreadLocal对象存活在jvm中,那么map中的线程Thread对象是不会被jvm垃圾回收的,所以容易出现内存泄露。
image.png

JDKThreadLocal实现

再ThreadLocal源码中并没有看到相关的可以去存储线程对象的集合变量,唯一的就是ThreadLocalMap能沾上边。
image.png
hashMap的get方法中存在获得map集合的相关代码,
image.png

1.getMap

可以看到这个threadLocal是从Thread这个对象中获取的
image.png
image.png
这个包含关系大致可以如下图所示,可以总结出ThreadLocal只是一个工具,而线程用这个来做本地化对象的操作。
image.png

2.map.getEntry

通过key做的计算从table中获取一个Entry

  1. private Entry getEntry(ThreadLocal<?> key) {
  2. int i = key.threadLocalHashCode & (table.length - 1);
  3. Entry e = table[i];
  4. if (e != null && e.get() == key)
  5. return e;
  6. else
  7. return getEntryAfterMiss(key, i, e);
  8. }

2的地方每个ThreadLocalMap持有一个Entry数组,且在1处每个Entry持有一个value对象。那整个Thread获取本地变量的一个流程就是线程Thread->ThreadLocalMap->Entry->value
在获取到ThreadLocalMap之后,会执行TheadLocalMap的getEntry方法,这个方法的参数是ThreadLocal类型。再根据传入的这个ThreadLocal的threadLocalHashCode计算坐标值,然后根据坐标值再从Entry数据里获取对应的Entry对象,从而获取到Entry里的value值。
image.png image.png

ThreadLocal内存泄露问题

最终存储的都是一个个Entry对象,而这个Entry对象继承一个包裹了ThreadLocal的弱引用对象,会发生一种情况就是说,当内存不够的时候发生GC了,那么这个存Entry对象中这些key值也就是“TheadLocal”就被回收掉了。因此整个Entry数组中情况就是,很多的Entry对像都是一种null-value的状态,然后entry对象还是在该Entry数组中没有被gc清除掉。因此就会发生内存泄漏。
JDK团队有解决的方案,就是在通过ThreadLocal执行set、get、remove方法时,他会自动清理掉entry数组里里值为null的key,确保不要有很多的null值引用了你的value造成内存的泄漏问题。

  1. /**
  2. * The entries in this hash map extend WeakReference, using
  3. * its main ref field as the key (which is always a
  4. * ThreadLocal object). Note that null keys (i.e. entry.get()
  5. * == null) mean that the key is no longer referenced, so the
  6. * entry can be expunged from table. Such entries are referred to
  7. * as "stale entries" in the code that follows.
  8. */
  9. static class Entry extends WeakReference<ThreadLocal<?>> {
  10. /** The value associated with this ThreadLocal. */
  11. Object value;
  12. Entry(ThreadLocal<?> k, Object v) {
  13. super(k);
  14. value = v;
  15. }
  16. }
  17. private void remove(ThreadLocal<?> key) {
  18. //使用hash方式,计算当前ThreadLocal变量所在table数组位置
  19. Entry[] tab = table;
  20. int len = tab.length;
  21. int i = key.threadLocalHashCode & (len-1);
  22. //再次循环判断是否在为ThreadLocal变量所在table数组位置
  23. for (Entry e = tab[i];
  24. e != null;
  25. e = tab[i = nextIndex(i, len)]) {
  26. if (e.get() == key) {
  27. //调用WeakReference的clear方法清除对ThreadLocal的弱引用
  28. e.clear();
  29. //清理key为null的元素
  30. expungeStaleEntry(i);
  31. return;
  32. }
  33. }
  34. }

使用TheadLocal的事项

1.一个长期存活的线程,线程池里的线程,要不然可能是你自己开启的线程在后台长期运行,尽量避免在ThreadLocal长期放入数据,你不使用的时候最好及时的进行remove,自己主动把数据给删除了。
2.共享资源不要被static所修饰。

使用场景

1.用于保存线程不安全的工具类,典型的就是这个SimpleDateFormat。
工具类使用ThreadLocal去存储。这种类有什么特点?就是这个类自己去生成一点产物并被当前线程去使用,如果这种类去共享了,那么不同线程使用的时候可能会得到相同的产物。这种想要在堆中存一份,使用的时候又不想被共享给别的线程的类就可以用ThreadLocal去为每个线程对象保存一份。
2.线程需要保存类似于全局变量的信息(在拦截器中获取用户的信息),可以让不同的方法直接使用,避免参数传递的麻烦缺不想被多线程共享。
一个线程对象的调用链中,可以用ThreadLocal去存变量值,去给别的调用方法使用。这种保存的数据实际在堆中分配的,又不能用这个对象做多线程操作,那么就为每个Thread对象留一份该数据的副本走完调用链路。就可以避免线程安全。这种情况下是侧重于避免传参。

ThreadLocal并不是解决共享资源多线程访问问题的

并不是。首先,多线程访问的本质就是多个线程访问同一个资源变量,最终的结果都要在堆中那一份变量上。
而ThreadLocal保证的是,给线程对象一个变量的副本,所有操作都在这个副本上,之后在线程对象的方法调用过程中都使用的是这一份变量并且这个变量不被别的线程访问到。因此,可以说用了ThreadLocal之后,线程操作的压根就不是共享资源了,而是自己独一份的资源,自然不会有线程安全问题。
使用过程中如果ThreadLocal包裹的对象被static修饰了,也是会有线程安全问题的,就变成共享的了。解释:static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。但如果是实例变量就可以存在多个副本。
还有一种情况不适用ThreadLocal的,就是给一个变量int a,然后多线程使用ThreadLocal去递增计算,计算完成后,又把这个变量从ThreadLocal中拿出来赋值给a,这种情况下也会出现线程安全问题。
可以思考一下,线程A获取了int a = 0这个副本,线程B获取了int a=0的副本。同时加一,再写回到原来的那个实例变量a中,就有可能线程A a=1赋值上去了,实例变量a变成了1,此时线程B现在是拿的a=0去做加一操作再去赋值回去,这样明显就不对啊。
所以说,ThreadLocal这个保证线程安全的方法不保证多线程操作后共享资源状态的正确性,而是保证线程运行中共享资源操作的正确性。(不保证全局正确,只保证单个线程对象中操作正确)
ThreadLocal就是避免资源竞争,用副本把共享资源隔离开
synchronized/各种锁,在资源竞争的情况下让线程串行化,同一时间只有一个线程可以访问该资源。