多线程对象及变量的并发访问
目标
本篇博文作为多线程技术的读书笔记,主要学习了以下知识点:
- synchronized对象监视器为Object时的使用
- synchronized对象监视器为Class时的使用
- 非线程安全是如何出现的
- 关键字volatile的主要作用
- 关键字volatile与synchronized的区别及使用
方法内的变量为线程安全
“非线程安全”问题存在于“实例变量”中,如果是方法内部的私有变量,则不存在“非线程安全”问题,这是方法内部的变量都是线程私有的特性造成的。
实例变量非线程安全
如果多个线程共同访问一个对象中的实例变量,则有可能出现“非线程安全”问题。
synchronized锁重入
关键字synchronized拥有锁重入功能,即当使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的,这也证明在一个synchronized方法/块的内部调用本来的其他synchronized方法/块时,是永远可以得到锁的。
可重入锁也同样适用于父子集成关系中,子类是完全可以通过“可重入锁”调用父类的同步方法的。
出现异常,锁自动释放
当一个线程执行的代码出现异常时,其所持有的锁会自动释放。
同步不具有继承性
如果父类某个方法是同步的,子类某个方法是非同步的,子类的非同步方法调用了父类中的同步方法,在多线程环境下调用子类的该同步方法,是无法同步的,必须子类的这个方法也实现同步。
synchronized同步语句块
用关键字synchronized声明方法在某些情况下是有弊端的,比如线程A调用同步方法执行一个长时间的任务,那么线程B线程则必须等待比较长时间的,即这个方法锁住的资源太多,锁粒度太大,不利于业务高效运行,这个时候我们就可以锁同步块。
synchronized代码块间的同步性
在使用同步synchronized(this)代码块时需要注意的是,当一个线程访问Object的一个synchronized(this)同步代码块时,其他线程对同一个Object中所有其他synchronzied(this)同步代码块的访问将被阻塞,这说明此时synchronized使用的是同一个对象监视器(同一个对象锁)。
synchronized(this)同步方法持有的是对象锁。
将任意对象作为对象监视器
多个线程调用同一个对象中的不同名称的synchronized同步方法或者synchronized(this)同步代码块时,调用的效果就是按顺序执行,也就是同步的,阻塞的。
在多个线程持有的对象监视器为同一个对象的前提下,同一时间只哟局一个线程可以执行synchronized(对象)同步方法/代码块中的内容。
锁非this对象具有一定的优先:如果在一个类中有很多个synchronized方法,这时虽然能实现同步,但会收到阻塞,所以影响运行效率;但如果使用代码块锁非this对象,则synchronized(对象)代码块中的程序与同步方法是异步的,不予其他锁this同步方法争抢this锁,则可以大大提高运行效率。
静态同步synchronized方法与synchronized(class)代码块
关键字synchronized应用在static静态方法上,那就是对当前*.java文件对应的Class类进行持锁。
synchronized public static void pringA(){}
synchronized关键字加到static静态方法上是给Class类上锁,而synchronized关键字加到非static静态方法上是给对象上锁。
数据类型String的常量池特性
在JVM中具有String常量池缓存的功能,比如以下:
1 String a = "a"; 2 String b = "a"; 3 System.out.println(a == b); 4 // true
即两个不同的String对象当时如果值相同,两个对象也是相等的,这就是String的常量池缓存特性。
当我们的synchronized(string)同步块与String联合使用时,就可能会应为这个常量池特性而带来不受控的一些影响了。因为如果两个不相干的线程在调用此次方法时,使用了相同的String值,那么原本不希望阻塞的异步方法此时也阻塞了。
1 public static void print(String str) { 2 synchronized (str) { 3 System.out.println(Thread.currentThread().getName()); 4 } 5 }
因此大多数情况下,同步synchronized代码块都不使用String作为锁对象,而改用其他如Object对象。
多线程的死锁
多线程的锁使用不当,很容易引起线程间的死锁,下面通过一个简单的例子来说明死锁。
1 public class MyThread09 extends Thread { 2 3 public String flag; 4 public Object lock1 = null; 5 public Object lock2 = null; 6 7 public MyThread09(String flag, Object lock1, Object lock2) { 8 this.flag = flag; 9 this.lock1 = lock1; 10 this.lock2 = lock2; 11 } 12 13 @Override 14 public void run() { 15 if(flag.equals("a")) { 16 synchronized (lock1) { 17 System.out.println("=== print A"); 18 try { 19 Thread.sleep(3000L); 20 } catch (InterruptedException e) { 21 e.printStackTrace(); 22 } 23 synchronized (lock2) { 24 System.out.println("=== print A again"); 25 } 26 } 27 } 28 if(flag.equals("b")) { 29 synchronized (lock2) { 30 System.out.println("=== print B"); 31 try { 32 Thread.sleep(5000L); 33 } catch (InterruptedException e) { 34 e.printStackTrace(); 35 } 36 synchronized (lock1) { 37 System.out.println("=== print B again"); 38 } 39 } 40 } 41 } 42 43 public static void main(String[] args) { 44 // lock1,lock2作为锁,或者说临界区资源 45 Object lock1 = new Object(); 46 Object lock2 = new Object(); 47 Thread threadA = new MyThread09("a", lock1, lock2); 48 Thread threadB = new MyThread09("b", lock1, lock2); 49 threadA.start(); 50 threadB.start(); 51 } 52 } 53 54 运行结果: 55 === print A 56 === print B 57 (程序无法停止)
当程序正在运行时,我们打开JDK自带的jps.exe来查看当前正在运行程序的ID值。
PS C:\Program Files\Java\jdk1.8.0_131\bin> .\jps.exe 25856 MyThread09 29152 Appliaction 34688 Jps 33972 Launcher 35780 Launcher
然后再执行jstack命令,查看具体的堆栈细节。
Found one Java-level deadlock: ============================= "Thread-1": waiting to lock monitor 0x000000001c30dc48 (object 0x000000076b39f1a8, a java.lang.Object), which is held by "Thread-0" "Thread-0": waiting to lock monitor 0x000000001c310638 (object 0x000000076b39f1b8, a java.lang.Object), which is held by "Thread-1" Java stack information for the threads listed above: =================================================== "Thread-1": at com.captainad.MyThread09.run(MyThread09.java:46) - waiting to lock <0x000000076b39f1a8> (a java.lang.Object) - locked <0x000000076b39f1b8> (a java.lang.Object) "Thread-0": at com.captainad.MyThread09.run(MyThread09.java:33) - waiting to lock <0x000000076b39f1b8> (a java.lang.Object) - locked <0x000000076b39f1a8> (a java.lang.Object) Found 1 deadlock.
从堆栈中可以看到程序已经发生了死锁,两个线程在持有不同的锁的情况下,还继续等待着获取对方持有的还没有释放的锁,形成了相互等待,最终造成程序死锁。
锁对象的改变
如果锁是String类型的,这个类型值后续改变了,锁也会随之改变,即争夺的资源就改变了。
如果锁的是对象,对象的属性在某个线程中改变了,线程间相互竞争的锁还是同一个对象的,不会因为对象的值改变而变更。
volatile关键字
关键字volatile的主要作用是使变量的变更在多线程间可见。
通过使用volatile关键字,强制线程从公共内存中读取变量的值,volatile关键字保证了变量在多线程间的可见性,但是该关键字不支持原子性。
下面将关键字synchronized和volatile做一个简单的比较:
- 关键字volatile是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好,并且volatile只能修饰于变量,而synchronized可以修饰方法以及代码块。随着JDK版本的升级,synchronized关键字在执行效率上得到了很大的提升,在后续使用synchronized关键字还是可以考虑的。
- 多线程访问volatile不会发生阻塞,而synchronized会发生阻塞。
- volatile能保证数据的可见性,但不能保证原子性;synchronized可以保证原子性,也可以间接保证可见性,因为他会将私有内存和公共内存中的数据做同步。
- 关键字volatile解决的是变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程之间访问资源的同步性。
volatile非原子特性
(Java多线程编程核心技术 截图)
1 volatile int i = 0; 2 Thread: i++;
在多线程环境下,即使给变量i加了volatile修饰,也是无法保证其计算的原子性的,上面的截图给出了解释,那么要做到一个多线程情况下使用的计数器该如何处理呢?
- 使用synchronized关键字来做同步
- 使用AtomicInteger原子类进行实现
总结
多线程的使用会产生线程安全问题,处理好线程安全问题也是我们学习多线程知识中需要掌握的一个技能。
关键字synchronized在处理同步块时,可能会出现多种情况,下面做个简单总结:
- 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。
- 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。
- 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。