Java多线程学习笔记(二)

对象及变量的并发访问

“非线程安全”其实会在多个线程对同一个对象中的实例变量进行并发访问时发生,产生的后果就是”脏读”,也就是取到的数据其实是被更改过的。而”线程安全”获得的实例变量的值是经过同步处理的,不会出现”脏读”的现象。

synchronized同步方法

方法内的私有变量

“非线程安全”问题存在于”实例变量”,如果是方法内部的私有变量,则不存在”非线程安全”问题,永远都是线程安全的,这是方法内部的变量是私有(作用域)的特性造成的。

实例变量

如果多个线程共同访问1个对象中的实例变量,则有可能出现”非线程安全”问题。我们需要在有可能产生”非线程安全”的方法前面加上synchronized关键字,将此方法变成同步方法。

关键字synchronized取得的锁都是对象锁,而不是把一段代码或者方法(函数)当作锁,哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象的锁Lock,那么其他线程只能呈现等待状态,前提是多个线程访问的是同一个对象。如果多个线程访问多个对象,则JVM会创建多个锁。

假设两个线程访问同一个对象的两个同步synchronized方法:

  1. A线程调用object对象加入synchronized关键字的X方法时,A线程就获得了X方法锁,准确的讲,是获得了对象的锁,所以其他线程必须等A线程执行完毕才可以调用X方法,但B线程可以随意调用其他的非synchronized同步方法。
  2. A线程调用object对象加入synchronized关键字的X方法时,A线程就获得了X方法所在对象的锁,所以其他线程必须等A线程执行完毕才可以调用X方法,而B线程如果调用声明了synchronized关键字的非X方法时,必须等A线程将X方法执行完,也就是释放了对象锁后才可以调用。

“可重入锁”:当有一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当这个线程再次想要获得这个对象的锁的时候还是可以获取的。特别的说明,当存在父子类继承关系时,子类是完全可以通过”可重入锁”调用父类的同步方法的。

但是,当一个线程执行的代码出现异常退出时,其所持有的锁会自动释放。

synchronized(this)同步语句块

用synchronized声明方法在时间效率上是有弊端的,比如A线程调用了同步方法执行一个长时间的任务,那么B线程则必须等待比较长的时间。

当两个并发线程访问同一个对象object中的synchronized(this)同步代码块时,一段时间内只能有一个线程被执行,另一个线程必须等待当前线程执行完这个代码块以后才能执行。并且当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object对象中的非synchronized(this)同步代码块。也就是说,不在synchronized(this)同步代码块就是异步执行,在synchronized块中就是同步执行。

当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对同一个object中所有其他synchronized(this)同步代码块的访问将被阻塞,这就说明synchronized使用的“对象监视器”是同一个。也就是说,和synchronized方法一样,synchronized(this)同步代码块也是锁定当前对象的。

对象监视器

在前面的学习中,使用synchronized(this)格式来同步代码块,其实我们还可以用”任意对象”作为”对象监视器”来实现同步的功能,使用的格式为synchronized(非this对象x)。那么,在多个线程持有”对象监视器”为同一个对象时,同一时间只有一个线程可以执行synchronized(非this对象x)同步代码块中的代码。如果不是同一个对象监视器,则会异步运行。更多示例参考《Java多线程编程核心技术》2.2.8节。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class MyOneList{
private List list = new ArrayList();
synchronized public void add(String data){
list.add(data);
}
synchronized public int getSize(){
return list.size();
}
}
class MyThread1 extends Thread{
private MyOneList list;
public MyThread1(MyOneList list){
super();
this.list = list;
}
public void run(){
try{
if(list.getSize() < 1){
Thread.sleep(2000);
list.add("A");
}
}catch(InterruptedException e ){
e.printStackTrace();
}
}
}
class MyThread2 extends Thread{
private MyOneList list;
public MyThread2(MyOneList list){
super();
this.list = list;
}
public void run(){
try{
if(list.getSize() < 1){
Thread.sleep(2000);
list.add("B");
}
}catch(InterruptedException e ){
e.printStackTrace();
}
}
}
public class Run{
public static void main(String[] args) throws InterruptedException {
MyOneList list = new MyOneList();
MyThread1 thread1 = new MyThread1(list);
thread1.setName("A");
thread1.start();
MyThread2 thread2 = new MyThread1(list);
thread1.setName("B");
thread1.start();
Thread.sleep(6000);
System.out.println("listSize = " + list.getSize());
}
}

上述代码就有可能出现”脏读”,导致输出结果为”listSize=2”,原因是2个线程以异步的方式返回list参数的size()大小,导致同时进入if判断语句中。

这个例子中如何解决”脏读”呢?由于list参数对象在项目中是一份实例,是单例的,而且也正需要对list参数的getSize()方法做同步调用,所以就对list参数进行同步处理。

更改代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyThread1 extends Thread{
private MyOneList list;
public MyThread1(MyOneList list){
super();
this.list = list;
}
public void run(){
try{
synchronized(list){
if(list.getSize() < 1){
Thread.sleep(2000);
list.add("A");
}
}
}catch(InterruptedException e ){
e.printStackTrace();
}
}
}

静态同步synchronized方法与synchronized(class)代码块

关键字synchronized还可以应用在static静态方法上,如果这样写,那是对当前的.java文件对应的Class类进行持锁。synchronized关键字加到static静态方法上是给Class类上锁,可以对类的所有对象实例起作用,而synchronized关键字加到非static静态方法上是给对象上锁,这2个锁不是同一个锁。synchronized(class)代码块的作用其实和synchronized static方法的作用一样。我们需要注意的是,synchronized(ClassName)与synchronized(ClassName的实例),线程各自获取各自的锁,不会有等待。

注意:我们在将任何数据类型作为同步锁时,需要观察,是否有多个线程同时持有锁对象,如果同时持有相同的锁对象,则这些线程之间就是同步的;如果分别获得锁对象,就是异步的。

volatile关键字

并发专家建议我们远离它,尤其是在JDK6的synchronized关键字的性能被大幅优化之后,更是几乎没有使用它的场景,但这仍然是个值得研究的关键字,研究它的意义不在于去使用它,而在于理解它对理解Java的整个多线程的机制是很有帮助的。

关键字volatile的主要作用是使变量在多个线程间可见。简单地说就是当线程A对变量X进行了修改后,在线程A后面执行的其他线程能看到变量X的变动。同时关键字synchronized也可以同样的完成volatile关键字可见性的功能。

更详细地说是要符合以下两个规则:

  • 线程对变量进行修改之后,要立刻回写到主内存。
  • 线程对变量读取的时候,要从主内存中读,而不是缓存。

这里我们需要提到Java内存模型。在Java内存模型中,内存分为主内存和工作内存两个部分,其中主内存是所有线程所共享的,而工作内存则是每个线程分配一份,各线程的工作内存间彼此独立、互不可见,在线程启动的时候,虚拟机为每个内存分配一块工作内存,不仅包含了线程内部定义的局部变量,也包含了线程所需要使用的共享变量(非线程内构造的对象)的副本,即为了提高执行效率,读取副本比直接读取主内存更快(这里可以简单地将主内存理解为虚拟机中的堆,而工作内存理解为栈(或称为虚拟机栈),栈是连续的小空间、顺序入栈出栈,而堆是不连续的大空间,所以在栈中寻址的速度比堆要快很多)。

对于共享普通变量来说,约定了变量在工作内存中发生变化了之后,必须要回写到工作内存(迟早要回写但并非马上回写),但对于volatile变量则要求工作内存中发生变化之后,必须马上回写到工作内存,而线程读取volatile变量的时候,必须马上到工作内存中去取最新值而不是读取本地工作内存的副本,此规则保证了前面所说的“当线程A对变量X进行了修改后,在线程A后面执行的其他线程能看到变量X的变动”。

volatile虽然保证了线程之间共享变量的及时可见性,但是并没有保证同步。也就是说,volatile不能保证原子性。而synchronized关键字解决的是多个线程之间访问资源的同步性。

这里说明一下,如果真的需要使用volatile关键字,那么,在禁止指令重排序是一个很好的场景。具体volatile关键字的用法,参考这篇博客:正确使用 Volatile 变量

0%