多线程学习总结(二)

一、多线程带来的问题

(一)活跃性问题

  1. 死锁:两个线程相互等待对方释放资源
  2. 饥饿: 多线程并发时优先级低的线程永远得不到执行;线程被永久阻塞在一个等待进入同步块的状态;等待的线程永远不被唤醒
  3. 活锁:活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程。处于活锁的实体是在不断的改变状态,活锁有可能自行解开。

如何避免饥饿问题:

  • 对于优先级引发的饥饿问题,用setPriority设置线程的优先级。 
  • 对于永久阻塞引发的饥饿问题,用锁来代替synchronized。

(二)线程安全性问题

1.什么是线程安全问题?

从某个线程开始访问到访问结束的整个过程,如果有一个访问对象被其他线程修改,那么对于当前线程而言就发生了线程安全问题;如果在整个访问过程中,无一对象被其他线程修改,就是线程安全的。

2.线程安全问题产生的根本原因?

  • 首先是多线程环境,即同时存在有多个操作者,单线程环境不存在线程安全问题。在单线程环境下,任何操作包括修改操作都是操作者自己发出的,操作者发出操作时不仅有明确的目的,而且意识到操作的影响。
  • 多个操作者(线程)必须操作同一个对象,只有多个操作者同时操作一个对象,行为的影响才能立即传递到其他操作者。
  • 多个操作者(线程)对同一对象的操作必须包含修改操作,共同读取不存在线程安全问题,因为对象不被修改,未发生变化,不能产生影响。

综上可知,线程安全问题产生的根本原因是共享数据存在被并发修改的可能,即一个线程读取时,允许另一个线程修改。

3.线程安全问题解决思路?

根据线程安全问题产生的条件,解决线程安全问题的思路是消除产生线程安全问题的环境:

  • 消除共享数据:成员变量与静态变量多线程共享,将这些全局变量转化为局部变量,局部变量存放在栈,线程间不共享,就不存在线程安全问题产生的环境了。消除共享数据的不足:如果需要一个对象采集各个线程的信息,或者在线程间传递信息,消除了共享对象就无法实现此目的。
  • 使用线程同步机制:给读写操作同时加锁,使得同时只有一个线程可以访问共享数据。如果单单给写操作加锁,同时只有一个线程可以执行写操作,而读操作不受限制,允许多线程并发读取,这时就可能出现不可重复读的情况,如一个持续时间比较长的读线程,相隔较长时间读取数组同一索引位置的数据,正好在这两次读取的时间内,一个线程修改了该索引处的数据,造成该线程从同一索引处前后读取的数据不一致。是同时给读写加锁,还是只给写加锁,根据具体需求而定。同步机制的缺点是降低了程序的吞吐量。
  • 建立副本:使用ThreadLocal为每一个线程建立一个变量的副本,各个线程间独立操作,互不影响。该方式本质上是消除共享数据思想的一种实现。

线程的优先级:

Thread类有三个优先级静态常量:MAX_PRIORITY为10,为线程最高优先级;MIN_PRIORITY取值为1,为线程最低优先级;NORM_PRIORITY取值为5,为线程中间位置的优先级。默认情况下,线程的优先级为NORM_PRIORITY;使用Thread的setPriority()方法来设置线程的优先级,设置的值越大(1~10),优先级越高,执行的几率越大。

(二)synchronized

  • 每个java对象都可以用做一个实现同步的锁,这些锁成为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法;

  • java内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去;

  • synchronized修饰普通方法,默认内置锁就是当前类的实例;

  • synchronized修饰静态方法,默认内置锁就是当前类的Class字节码对象;

  • synchronized修饰代码块,锁可以为一个实例对象或者一个类的字节码文件;

  • 任何对象都可以作为锁,锁信息存在于对象头(Mark Word)中。

(三)volatile:

1.volatile的作用

  • 保证线程间变量的可见性(不保证原子性):线程对变量进行修改之后,要立刻回写到主内存;线程对变量读取的时候,要从主内存中读,而不是缓存。

Java为了保证其平台性,使Java应用程序与操作系统内存模型隔离开,需要定义自己的内存模型。在Java内存模型中,内存分为主内存和工作内存两个部分,其中主内存是所有线程所共享的,而工作内存则是每个线程分配一份,各线程的工作内存间彼此独立、互不可见,在线程启动的时候,虚拟机为每个内存分配一块工作内存,不仅包含了线程内部定义的局部变量,也包含了线程所需要使用的共享变量(非线程内构造的对象)的副本,即为了提高执行效率,读取副本比直接读取主内存更快(这里可以简单地将主内存理解为虚拟机中的堆,而工作内存理解为栈(或称为虚拟机栈),栈是连续的小空间、顺序入栈出栈,而堆是不连续的大空间,所以在栈中寻址的速度比堆要快很多)。工作内存与主内存之间的数据交换通过主内存来进行,如下图:QQ截图20131228132842

同时,Java内存模型还定义了一系列工作内存和主内存之间交互的操作及操作之间的顺序的规则(这规则比较多也比较复杂,参见《深入理解Java虚拟机-JVM高级特性与最佳实践》第12章12.3.2部分),这里只谈和volatile有关的部分。对于共享普通变量来说,约定了变量在工作内存中发生变化了之后,必须要回写到工作内存(迟早要回写但并非马上回写),但对于volatile变量则要求工作内存中发生变化之后,必须马上回写到工作内存,而线程读取volatile变量的时候,必须马上到工作内存中去取最新值而不是读取本地工作内存的副本,此规则保证了前面所说的“当线程A对变量X进行了修改后,在线程A后面执行的其他线程能看到变量X的变动”。

(链接:https://www.cnblogs.com/xll1025/p/6486170.html

  • 禁止指令重排序

2. volatile的适用场景

  • volatile是在synchronized性能低下的时候提出的。如今synchronized的效率已经大幅提升,所以volatile存在的意义不大。
  • 如今非volatile的共享变量,在访问不是超级频繁的情况下,已经和volatile修饰的变量有同样的效果了。
  • volatile不能保证原子性,这点是大家没太搞清楚的,所以很容易出错。
  • volatile可以禁止重排序。

 

 

 

 

 

(注:只做个人学习总结,如有侵权请联系我,有的资料是网上获取的。。。。。)

猜你喜欢

转载自blog.csdn.net/qq_37776015/article/details/82533024