这是我参与更文挑战的第3天,活动详情查看: 更文挑战
1. Synchronized使用
保证在同一时刻只有一个线程可以执行某个方法或者代码块(用于操作共享变量),属于互斥锁。synchronized可以用来修饰方法,代码块。分为以下三种:
- 修饰实例方法。在当前实例对象上加锁,线程进入当前方法前需要获得实例对象的锁,称之为对象锁。
- 修饰静态方法。在前类的class对象上加锁,线程进入静态方法前需要获得类对象的锁,称之为类锁。
- 修饰代码块。在实例对象上加锁,称之为对象锁。在类的class对象上加锁,称之为类锁,保证同一时刻只有一个线程获得锁。
1. 修饰实例方法
synchronized修饰的实例方法,指的是非静态方法,某一个线程获得锁时,获取的是对象锁。
public class SyncTask implements Runnable {
static int count = 0;
@Override
public void run() {
increase();
}
private synchronized void increase() {
for (int i = 0; i < 1000000; i++) {
count++;
}
}
}
//执行类
SyncTask syncTask =new SyncTask();
Thread thread1=new Thread(syncTask,"thread1");
Thread thread2=new Thread(syncTask,"thread2");
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(" count = " + SyncTask.count);
//结果为
count = 2000000
复制代码
可以得出结论多个线程在操作同一个对象的方法时,synchronized修饰的实例方法在同一时刻只有一个线程可以获得锁,当前线程执行完毕释放锁后,其他线程才可以竞争锁并执行。
思考一下:当多个线程操纵的是不同的对象的synchronized修饰的方法时,会怎么样?
public static void main(String[] args){
Thread thread1=new Thread(new SyncTask(),"thread1");
Thread thread2=new Thread(new SyncTask(),"thread2");
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(" count = " + SyncTask.count);
}
//执行结果
count = 1197724
复制代码
上述代码改变在于不同的线程操纵不同的对象。需要注意到的是SyncTask的synchronized修饰的方法内部操作的是静态变量count。可以看到结果并不是我们想象的2000000。因此可以得出结论:不同线程在操作不同对象的synchronized修饰的方法时互不影响。作用的是当前各自的对象,因此不存在竞争锁的情况,导致了静态变量count的值与预想的有较大出入。相当于不同线程访问同一对象的不带synchronized修饰的increase方法。
2. 修饰静态方法
synchronized修饰的静态方法,获取的是当前的类的class对象锁,也叫类锁。static修饰的成员变量和成员方法属于类独有,因此通过class对象可以保证static方法的并发操作。需要注意的是当线程A操作一个实例对象的非static synchronized修饰的方法和线程B操作类对象的static synchronize方法时是不冲突的。不会发生互斥。因为线程A操作的是对象锁,而线程B操作的是类锁。
public class SyncTask implements Runnable {
static int count = 0;
@Override
public void run() {
increase();
}
private synchronized static void increase() {
for (int i = 0; i < 1000000; i++) {
count++;
}
}
}
//执行函数
public static void main(String[] args){
SyncTask syncTask = new SyncTask();
Thread thread1=new Thread(new SyncTask(),"thread1");
Thread thread2=new Thread(new SyncTask(),"thread2");
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(" count = " + SyncTask.count);
}
//执行结果
count = 2000000
复制代码
synchronized修饰的是静态的increase方法,获得的是类锁,和实例方法不同,实例方法获取的是对象锁。无论是操作不同的对象还是同一个对象,如果操作的是类锁,那么同一时刻只有一个线程获得锁。
3. 修饰代码块
如果某一个方法中方法体较多,只有一些部分需要同步执行,此时就可以采用同步代码快的方法来实现部分代码块同步。同步代码块可以采用对象锁,锁住当前对象,或者采用类锁,锁住整个类。
public class SyncTask implements Runnable {
static int count = 0;
static SyncTask syncTask = new SyncTask();
@Override
public void run() {
increase();
}
private void increase() {
synchronized(syncTask){
for (int i = 0; i < 1000000; i++) {
count++;
}
}
}
}
//执行函数
public static void main(String[] args){
SyncTask syncTask = new SyncTask();
Thread thread1=new Thread(new SyncTask(),"thread1");
Thread thread2=new Thread(new SyncTask(),"thread2");
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(" count = " + SyncTask.count);
}
//执行结果
count = 2000000
复制代码
在increase中synchronized锁住的是syncTask对象,每一个进入increase方法的线程都必须持有syncTask对象锁才可以继续执行,否则只能等待。synchronized中也可以使用this表明锁住的是当前对象,但是我们要看到执行函数中每个线程操作的是不同的对象,因此此时的执行结果就不再是预期结果了。此时我们可以采用类的class对象(类锁)的方式来保证同一时刻只有一个线程获得锁。若是在static
方法中,因为static方法中只能使用静态成员变量和类的class对象,所以可以创建一个静态成员变量或者直接采用类的class对象来保证同步。
4. 类锁和对象锁的使用区别
- 多个线程操作同一对象时,synchronized可以锁定一个对象、this关键字或者类的class对象都可以;也就是对象锁,类锁都有互斥作用。
- 多个线程操作不同对象时,synchronized可以锁定一个静态对象或者采用类的class对象。this关键字由于作用的是当前线程操作的实例对象,所以不存在互斥锁作用。
- 多个线程无论是操作同一对象还是多个对象,都可以采用锁定一个静态对象或者类的class对象。例如上面的syncTask对象属于对象锁。而类的class对象属于类锁,都可以产生作用。
2. Synchronized原理
synchronized的实现是基于Monitor来实现的。synchronized在修饰同步方法和同步代码块时原理不同。
2.1 Synchronized修饰代码块原理
在SyncTask的run方法中创建了一个同步代码块
public class SyncTask implements Runnable {
static int count = 0;
static SyncTask syncTask = new SyncTask();
@Override
public void run() {
synchronized(syncTask){
for (int i = 0; i < 1000000; i++) {
count++;
}
}
}
}
复制代码
使用javap -verbose class反编译class文件可以获取字节码,这里只截取了run方法的字节码:
public void run();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: getstatic #2 // Field syncTask:Lcom/mdy/lib/SyncTask;
3: dup
4: astore_1
5: monitorenter //此处进入同步方法
6: iconst_0
7: istore_2
8: iload_2
9: ldc #3 // int 1000000
11: if_icmpge 28
14: getstatic #4 // Field count:I
17: iconst_1
18: iadd
19: putstatic #4 // Field count:I
22: iinc 2, 1
25: goto 8
28: aload_1
29: monitorexit //此处退出同步方法
30: goto 38
33: astore_3
34: aload_1
35: monitorexit //此处退出同步方法
36: aload_3
37: athrow
38: return
复制代码
可以看到在同步代码块中使用monitorenter
和monitorexit
来实现。monitorenter指向同步代码块的起始位置,monitorexit指向同步代码块的结束位置。执行到monitorenter时,当前线程会尝试获得objectref
(对象锁)对应的的monitor持有权,当objectref的monitor的进入计数器值为0时,当前线程获得monitor,计数器值+1,当前线程获得锁。如果当前线程已经持有monitor,则会重入这个monitor,计数器值+1。倘若其他线程持有objectref的monitor,则当前线程进入阻塞状态。直到其他线程执行完毕,即monitorexit指令被执行,进入计数器值为0,此时其他线程将有机会持有锁。
wait的理解
ObjectMonitor() {
...
_WaitSet = NULL; //处于wait状态的线程集合
_owner // 拿到锁的线程
_EntryList = NULL ; //处于等待锁block状态的线程集合
...
}
复制代码
- 当一个线程申请锁时,就会进入_EntryList集合等待,参与锁竞争。
- 当线程获得锁时,_owner就会标记获得锁的线程
- 获得锁的线程在调用
wait
方法时,会释放锁,进入_WaitSet中,等待被唤醒。 - 当_WaitSet中线程被唤醒时,重新进入_EntryList集合中,参与竞争锁。
2.2 Synchronized修饰方法原理
SyncTask使用synchronized来修饰同步方法
public class SyncTask implements Runnable {
static int count = 0;
@Override
public void run() {
inCrease();
}
public synchronized void inCrease(){
for (int i = 0; i < 1000000; i++) {
count++;
}
}
}
复制代码
使用javap -verbose class反编译class文件可以获取字节码,这里只截取了inCrease方法的字节码:
public synchronized void inCrease();
descriptor: ()V
flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=2, args_size=1
0: iconst_0
1: istore_1
2: iload_1
3: ldc #3 // int 1000000
5: if_icmpge 22
8: getstatic #4 // Field count:I
11: iconst_1
12: iadd
13: putstatic #4 // Field count:I
16: iinc 1, 1
19: goto 2
22: return
复制代码
反编译后的字节码中,被synchronized修饰的同步方法内部并没有monitorenter和monitorexit指令,使用ACC_SYNCHRONIZED
来标识该方法是不是一个同步方法。
2.3 Synchronized的优化
在Java6 之前synchronized属于重量级锁,在Java6之后为了减少获得锁和释放锁带来的性能消耗,又加入了偏向锁、轻量级锁。