为什么要锁
如果多个线程修改同一个数据对象,那么会对数据造成破坏,出现“意料之外”的结果,显然不是我们想要,所以我们需要线程同步,也就是需要一把“锁”。
举个例子:
package com.johan.syncthread;
public class MainApp {
public static void main(String[] args) {
DataClass dataClass = new DataClass(100);
Thread thread1 = new Thread(new AddDataRunnale(dataClass));
Thread thread2 = new Thread(new AddDataRunnale(dataClass));
Thread thread3 = new Thread(new AddDataRunnale(dataClass));
Thread thread4 = new Thread(new AddDataRunnale(dataClass));
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
static class AddDataRunnale implements Runnable {
private DataClass dataClass;
public AddDataRunnale(DataClass dataClass) {
this.dataClass = dataClass;
}
@Override
public void run() {
for (int i = 1; i < 3; i++) {
int newData = dataClass.add(i * 10);
System.out.println("+ " + i * 10 + " = " + newData);
}
}
}
static class DataClass {
private int data;
public DataClass(int data) {
this.data = data;
System.out.println("data = " + data);
}
public int add(int data) {
this.data += data;
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return this.data;
}
public int getData() {
return data;
}
}
}
这里是4个线程同时操作DataClass对象的data属性,看一下打印结果:
data = 100
+ 10 = 140
+ 10 = 140
+ 10 = 140
+ 10 = 140
+ 20 = 220
+ 20 = 220
+ 20 = 220
+ 20 = 220
这结果显然每次加完的结果是不对的,造成“脏读”现象。我们现在把DataClass的add方法设置为同步方法:
public synchronized int add(int data) {
this.data += data;
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return this.data;
}
我们再来看看打印结果:
data = 100
+ 10 = 110
+ 10 = 120
+ 20 = 140
+ 10 = 150
+ 10 = 160
+ 20 = 180
+ 20 = 200
+ 20 = 220
最终的结果正确,而且每次加完之后,读取到的也是正确的结果。
通过这个例子,应该认识到多线程同步的重要性了吧。
锁机制
每个Java对象都有一个锁,当有一个线程(A)调用一个实例(如dataClass)的某个非静态synchronized同步方法(例如add)时,此时便获得了这个实例(dataClass)的对象锁。此时,如果有其他线程(B)也想调用该实例(dataClass)的非静态synchronized同步方法(任何一个),因为该实例(dataClass)对象锁已经被前一个线程(A)持有,所以其他线程(B)只能阻塞,进入该对象的锁等待池中,等待获取对象锁。直到前一个线程(A)运行完某个非静态synchronized同步方法(add),就会释放对象锁,其他线程(B)才能开始运行该实例的非静态synchronized同步方法(任何一个)。注意,对于非静态synchronized同步方法,实例(dataClass)便是对象锁。
等同于以下同步代码块:
public int add(int data) {
// 同步的是this,每个实例this不同,所以每个实例有自己的对象锁
synchronized (this) {
this.data += data;
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return this.data;
}
}
对于静态方法加同步时,原理和非静态不同方法差不多,只是此时的对象锁是整个Class类,在上面的例子中,就是DataClass这个类。
等同于以下同步代码块:
public static int add(int data) {
synchronized (DataClass.class) {
this.data += data;
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return this.data;
}
}
- 调用同一个对象中非静态同步方法的线程将彼此阻塞。如果是不同对象,则每个线程有自己的对象的锁,线程间彼此互不干预。
- 调用同一个类中的静态同步方法的线程将彼此阻塞,它们都是锁定在相同的Class对象上。
再说说同步方法和同步代码块的区别:主要是同步的粒度不一样。同步方法,会同步整个方法,粒度比较大。而同步代码块存在于方法里,线程只在运行到同步代码块时才去获取锁,粒度较小。
死锁
来个经典的死锁例子:
package com.johan.syncthread;
public class MainApp {
public static void main(String[] args) {
Object lockA = new Object();
Object lockB = new Object();
Thread threadA = new Thread(new RunnableA(lockA, lockB));
Thread threadB = new Thread(new RunnableB(lockA, lockB));
threadA.start();
threadB.start();
}
public static class RunnableA implements Runnable {
private Object lockA, lockB;
public RunnableA(Object lockA, Object lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.err.println("RunnableA get lockA");
synchronized (lockB) {
System.err.println("RunnableA get lockB");
}
}
}
}
public static class RunnableB implements Runnable {
private Object lockA, lockB;
public RunnableB(Object lockA, Object lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockB) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.err.println("RunnableB get lockB");
synchronized (lockA) {
System.err.println("RunnableB get lockA");
}
}
}
}
}
我们先来看结果:
RunnableA get lockA
RunnableB get lockB
两个线程都只获取了第一个锁,程序便执行不下去了。聪明的你一定发现了猫腻。
我们分析一下,RunnableA获取lockA之后,等待了100毫秒之后,再想获取lockB,无奈此时的lockB已经被RunnableB获取了,所以RunnableA一直再等,但是又不释放lockA。而RunnableB获取到了lockB后,同样等待了100毫秒,想要获取lockA,此时的lockA又被RunnableA持有了,同样只能等。
现在的情况是:
RunnableA持有lockA,等lockB。
RunnableB持有lockB,等lockA。
那么RunnableA和RunnableB两个线程只能耗着,这就造成了“死锁”。
造成死锁的主要原因是,线程间互相等待锁,只要尽量避免多线程同时持有多个锁。即使同时持有多个锁也无所谓,但是不要抱死不放就行,充分利用对象的wait和notify方法。
Java设计的锁
其实Java设计了几种锁供我们使用,其中比较常用的是ReentrantLock和ReadWriteLock。
这里引用博客【Java线程】锁机制:synchronized、Lock、Condition
(1)ReentrantLock
class Outputter1 {
private Lock lock = new ReentrantLock();// 锁对象
public void output(String name) {
lock.lock(); // 得到锁
try {
for(int i = 0; i < name.length(); i++) {
System.out.print(name.charAt(i));
}
} finally {
lock.unlock();// 释放锁
}
}
}
ReentrantLock 类实现了Lock ,它拥有与synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)
需要注意的是,用sychronized修饰的方法或者语句块在代码执行完之后锁自动释放,而是用Lock需要我们手动释放锁,所以为了保证锁最终被释放(发生异常情况),要把互斥区放在try内,释放锁放在finally内!!
(2)读写锁ReadWriteLock
class Data {
private int data;// 共享数据
private ReadWriteLock rwl = new ReentrantReadWriteLock();
public void set(int data) {
rwl.writeLock().lock();// 取到写锁
try {
System.out.println(Thread.currentThread().getName() + "准备写入数据");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.data = data;
System.out.println(Thread.currentThread().getName() + "写入" + this.data);
} finally {
rwl.writeLock().unlock();// 释放写锁
}
}
public void get() {
rwl.readLock().lock();// 取到读锁
try {
System.out.println(Thread.currentThread().getName() + "准备读取数据");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "读取" + this.data);
} finally {
rwl.readLock().unlock();// 释放读锁
}
}
}
public static void main(String[] args) {
final Data data = new Data();
//写入
for (int i = 0; i < 3; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 5; j++) {
data.set(new Random().nextInt(30));
}
}
});
t.setName("Thread-W" + i);
t.start();
}
//读取
for (int i = 0; i < 3; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 5; j++) {
data.get();
}
}
});
t.setName("Thread-R" + i);
t.start();
}
}
结果:
Thread-W1准备写入数据
Thread-W1写入9
Thread-W1准备写入数据
Thread-W1写入24
Thread-W1准备写入数据
Thread-W1写入12
Thread-W0准备写入数据
Thread-W0写入22
Thread-W0准备写入数据
Thread-W0写入15
Thread-W0准备写入数据
Thread-W0写入6
Thread-W0准备写入数据
Thread-W0写入13
Thread-W0准备写入数据
Thread-W0写入0
Thread-W2准备写入数据
Thread-W2写入23
Thread-W2准备写入数据
Thread-W2写入24
Thread-W2准备写入数据
Thread-W2写入24
Thread-W2准备写入数据
Thread-W2写入17
Thread-W2准备写入数据
Thread-W2写入11
Thread-R2准备读取数据
Thread-R1准备读取数据
Thread-R0准备读取数据
Thread-R0读取11
Thread-R1读取11
Thread-R2读取11
Thread-W1准备写入数据
Thread-W1写入18
Thread-W1准备写入数据
Thread-W1写入1
Thread-R0准备读取数据
Thread-R2准备读取数据
Thread-R1准备读取数据
Thread-R2读取1
Thread-R2准备读取数据
Thread-R1读取1
Thread-R0读取1
Thread-R1准备读取数据
Thread-R0准备读取数据
Thread-R0读取1
Thread-R2读取1
Thread-R2准备读取数据
Thread-R1读取1
Thread-R0准备读取数据
Thread-R1准备读取数据
Thread-R0读取1
Thread-R2读取1
Thread-R1读取1
Thread-R0准备读取数据
Thread-R1准备读取数据
Thread-R2准备读取数据
Thread-R1读取1
Thread-R2读取1
Thread-R0读取1
与互斥锁定相比,读-写锁定允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程)。使用读-写锁定所允许的并发性增强将带来更大的性能提高。
以后会对锁机制有更多的理解,慢慢补回来!!!