Java多线程编程的入门篇,主要介绍volatile修饰词、Synchronized以及Lock及其子类
多线程编程主要存在的问题是数据的同步问题,下面我们讲讲几种保证数据同步的方法
volatile
要想了解volatile修饰词就必须先说说并发编程中的3个概念:
原子性
原子性指的是:多个操作要么全部执行不可中断,要么全部不执行。
可见性
可见性指的是:多线程操作同一个数据时,
其中一个线程操作完数据其他线程能立即看到数据的改变
(java中通过volatile来保证)。
有序性
有序性指的是:程序执行过程按照代码的顺序执行,
虽然编译器和处理器可能会对指令进行重排序,
但是保证执行的结果相同。
再来了解一下java的内存模型,就可以知道java怎么通过volatile来保证数据的可见性
java的内存模型
Java内存模型中规定,所有的数据都存储在主内存中,
线程对要操作的数据从主线程拷贝到自己的工作内存。
每个线程只能操作自己工作内存中的数据,不能直接操作主内存中的数据,
也不能访问其他线程的工作内存。
同时线程操作的数据可能存在缓存中,并不直接刷新到主内存中,
从上面可以知道,如果不保证数据的可见性,多个线程一起操作同一份数据时,并不能保证线程从主线程拷贝下来的数据是最新的,这样会让计算结果产生偏差。这个时候volatile就起作用了,被volatile修饰的变量,在被线程操作时数据的改变能直接刷新到主内存中,保证这个操作产生的新数据对其他线程是可见的,其他线程从主内存读取到是最新的数据。
同时volatile还有另外一个作用,被volatile修饰的变量将不能被编译器和处理器指令重排序。也就是volatile之后的语句不能提前到volatile之前,同时volatile之前的语句也不能排列到volatile之后。
示例:
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}
Synchronized
Synchronized同步锁比较好理解,可以作用于代码块、成员方法以及静态方。但是要求锁住的部分,持有的是同一个锁对象,这时才能Synchronized起作用。
作用于代码块
某些情况下,我们可能在方法的一部分出现耗时操作,为了避免锁住整个方法可以用Synchronized同步代码块
public class SynchronizedTest {
public static int sInt = 0;
public static void main(String[] args) {
new Thread(new MyRunnable(SynchronizedTest.class, false)).start();
new Thread(new MyRunnable(SynchronizedTest.class, true)).start();
}
private static class MyRunnable implements Runnable {
private final Object lock;
boolean flag;
public MyRunnable(Object lock, boolean flag) {
this.lock = lock;
this.flag = flag;
}
@Override
public void run() {
if (flag) {
synchronized (lock) {
for (int i = 0; i < 1000; i++) {
sInt++;
}
System.out.println("calc finish");
}
} else {
System.out.println("hello world");
}
}
}
}
打印如下:
hello world 1525252712746
calc finish 1525252712747
可以看出同步访问
作用于成员方法
这个很好理解,我们需要锁住一整个方法,直接在方法上添加Synchronized即可
public class SynchronizedTest {
public static int sInt = 0;
public static void main(String[] args) {
SynchronizedTest test = new SynchronizedTest();
new Thread(new Runnable() {
@Override
public void run() {
test.Test();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
test.Test();
}
}).start();
}
private synchronized void Test() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("Test finish at " + System.currentTimeMillis());
}
}
}
打印如下:
Test finish at 1525253058767
Test finish at 1525253060770
可以看出两者的打印差了2秒以上。
这里需要特别说明一下,当Synchronized作用在成员方法上时其实是有锁对象的,持有的对象为当前的实例对象,这个和作用在静态方法上时有所区别,这个等下说。先看一下,不同实例时,成员方法上的锁是否生效。
public class SynchronizedTest {
public static int sInt = 0;
public static void main(String[] args) {
SynchronizedTest testOne = new SynchronizedTest();
SynchronizedTest testTwo = new SynchronizedTest();
new Thread(new Runnable() {
@Override
public void run() {
testOne.Test();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
testTwo.Test();
}
}).start();
}
private synchronized void Test() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("Test finish at " + System.currentTimeMillis());
}
}
}
打印如下:
Test finish at 1525253495010
Test finish at 1525253495010
同时执行完毕,不同对象时锁依然存在,只是持有的锁对象不是同一个,所以才会同步打印。
作用于静态方法
public class SynchronizedTest {
public static int sInt = 0;
public static void main(String[] args) {
SynchronizedTest testOne = new SynchronizedTest();
SynchronizedTest testTwo = new SynchronizedTest();
new Thread(new Runnable() {
@Override
public void run() {
testOne.Test();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
testTwo.Test();
}
}).start();
}
public synchronized static void Test() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("Test finish at " + System.currentTimeMillis());
}
}
}
打印如下:
Test finish at 1525253718091
Test finish at 1525253720095
这里和synchronized作用于成员方法时有区别,当synchronized作用于静态方法时,持有的锁对象为当前类的.class对象,所有不同的实例来操作都是同步的,因为.class对象是唯一的。
synchronized可重入性
先看一个示例
public class SynchronizedTest {
private final Object mLock = new Object();
public static void main(String[] args) {
new SynchronizedTest().TestOne();
System.out.println();
new SynchronizedTest().TestFour();
}
private synchronized void TestOne() {
System.out.println("Test One");
TestTwo();
}
private synchronized void TestTwo() {
System.out.println("Test Two");
TestThree();
}
private synchronized void TestThree() {
System.out.println("Test Three");
}
private void TestFour() {
synchronized (mLock) {
System.out.println("Test Four");
TestFive();
}
}
private void TestFive() {
synchronized (mLock) {
System.out.println("Test Five");
}
}
}
打印如下:
Test One
Test Two
Test Three
Test Four
Test Five
可以看出,程序并没有产生死锁,正常输入,这就是synchronized可重入性
在讲Lock前,先讲几个Object的方法
wait()、notify()以及notifyAll()
这几个方法是Object中的方法,所有每个锁对象都能调用
- wait()只有在同步代码块或者同步方法中才能调用,因为要保证当前线程持有锁对象。wait()的作用是让当前执行代码的线程进入等待状态,直到为唤起或者中断。
- notify()作用是将等待状态的线程唤起,让它等待获取锁对象。不同于notifyAll(),如果有多个线程需要唤起,notify()会随机挑选一个线程进行唤起,其他线程继续等待。notify()也需要获取到锁对象,同时被唤起的线程也不能立即被执行,因为需要等待notify()的线程执行完毕并释放锁。
- notifyAll()唤醒所有的等待线程,等到notifyAll()释放锁后同步执行
看一个notify的示例:
public class SynchronizedTest {
public static void main(String[] args) {
try {
new Thread(new RunnableOne(Object.class)).start();
Thread.sleep(1000);
new Thread(new RunnableTwo(Object.class)).start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static class RunnableOne implements Runnable {
private final Object mLock;
RunnableOne(Object lock) {
mLock = lock;
}
@Override
public void run() {
synchronized (mLock) {
try {
System.out.println("wait start " + System.currentTimeMillis());
mLock.wait();
System.out.println("wait finish " + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
private static class RunnableTwo implements Runnable {
private final Object mLock;
RunnableTwo(Object lock) {
mLock = lock;
}
@Override
public void run() {
synchronized (mLock) {
try {
Thread.sleep(2000);
mLock.notify();
System.out.println("notify thread " + System.currentTimeMillis());
Thread.sleep(2000);
System.out.println("notify finish " + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
打印如下:
wait start 1525261601930
notify thread 1525261604937
notify finish 1525261606940
wait finish 1525261606941
可以清楚的看到notify结束后wait才执行,符合我们的预期
再来看一个notifyAll的示例,将上面的代码启动多个RunnableOne,notify()改成notifyAll()
打印如下:
wait start 1525261838049
wait start 1525261838049
wait start 1525261838050
wait start 1525261838050
wait start 1525261838050
notify thread 1525261841060
notify finish 1525261843065
wait finish 1525261843065
wait finish 1525261843065
wait finish 1525261843065
wait finish 1525261843065
wait finish 1525261843066
死锁
死锁指的是某一线程一直持有锁对象不释放,其他线程无法获取锁对象的过程。产生死锁一般的原因都是多个锁对象一起使用,同时相互请求锁对象导致的。
看个示例:
public class SynchronizedTest {
public static final Object LOCK_ONE = new Object();
public static final Object LOCK_TWO = new Object();
public static void main(String[] args) {
try {
new Thread(new RunnableOne()).start();
Thread.sleep(1000);
new Thread(new RunnableTwo()).start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static class RunnableOne implements Runnable {
@Override
public void run() {
try {
synchronized (LOCK_ONE) {
Thread.sleep(2000);
synchronized (LOCK_TWO) {
System.out.println(System.currentTimeMillis() + "");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private static class RunnableTwo implements Runnable {
@Override
public void run() {
try {
synchronized (LOCK_TWO) {
Thread.sleep(2000);
synchronized (LOCK_ONE) {
System.out.println(System.currentTimeMillis() + "");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
直接死锁无打印,因为这里第一个线程启动的时候获取到了LOCK_ONE,接着睡眠,线程二启动,获取到了LOCK_TWO,睡眠。当第一个线程再次起来去获取LOCK_TWO时,等待LOCK_TWO被释放;当第二个线程再次起来去获取LOCK_ONE时,等待LOCK_ONE释放,相互等待对方释放锁,也即进入死锁状态。
Lock
Lock只是定义的一个接口,直接实现类是ReentrantLock,相对于synchronized锁,Lock可以在需要的情况下自我中断。下面来看一下ReentrantLock使用
public class LockTest {
public static void main(String[] args) {
try {
ReentrantLock reentrantLock = new ReentrantLock();
new Thread(new RunnableOne(reentrantLock)).start();
Thread.sleep(1000);
new Thread(new RunnableTwo(reentrantLock)).start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static class RunnableOne implements Runnable {
final ReentrantLock mLock;
public RunnableOne(ReentrantLock lock) {
mLock = lock;
}
@Override
public void run() {
try {
mLock.lock();
System.out.println("RunnableOne get lock " + System.currentTimeMillis());
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
mLock.unlock();
}
}
}
private static class RunnableTwo implements Runnable {
final ReentrantLock mLock;
public RunnableTwo(ReentrantLock lock) {
mLock = lock;
}
@Override
public void run() {
try {
mLock.lock();
System.out.println("RunnableTwo get lock " + System.currentTimeMillis());
} finally {
mLock.unlock();
}
}
}
}
打印如下:
RunnableOne get lock 1525316728346
RunnableTwo get lock 1525316733350
线程2一直阻塞,等到线程1释放锁后才进行了打印,这和synchronized没区别
公平锁与非公平锁
当然ReentrantLock还有其他的特性,比如可以在实例化时指定是否为公平锁。公平锁和非公平锁的区别就是,公平锁所有线程按入队的顺序排队获取锁,非公平锁就是抢占机制,先入队并不一定获取到锁。
public class LockTest {
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock(false);
Thread[] threads = new Thread[5];
for (int i = 0; i < 5; i++) {
threads[i] = new Thread(new RunnableOne(reentrantLock));
}
for (Thread thread : threads) {
thread.start();
}
}
private static class RunnableOne implements Runnable {
final ReentrantLock mLock;
RunnableOne(ReentrantLock lock) {
mLock = lock;
}
@Override
public void run() {
try {
mLock.lock();
System.out.println(Thread.currentThread().getName() + " get lock ");
} finally {
mLock.unlock();
}
}
}
}
非公平锁的情况下,打印如下:
Thread-0 get lock
Thread-4 get lock
Thread-1 get lock
Thread-2 get lock
Thread-3 get lock
提前启动的线程并没有先得到锁
再来看一下公平锁,只需要将ReentrantLock实例化时fair参数传入true即可,看一下打印:
Thread-0 get lock
Thread-1 get lock
Thread-2 get lock
Thread-3 get lock
Thread-4 get lock
符合先启动的线程先获取锁的预期。
Lock中断
相对于synchronized对线程中断的不处理,Lock还有一个方法lockInterruptibly(),可以让用户在必要的情况下自行中断对锁的请求,然后继续后续的操作。java的中断机制后续再写
来看一个例子
public class LockTest {
public static void main(String[] args) {
try {
ReentrantLock reentrantLock = new ReentrantLock();
new Thread(new Runnable() {
@Override
public void run() {
reentrantLock.lock();
}
}).start();
Thread.sleep(1000);
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
reentrantLock.lockInterruptibly();
} catch (InterruptedException e) {
System.out.println("我被中断了");
}
}
});
thread.start();
Thread.sleep(1000);
thread.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
打印如下:
我被中断了
很明显的一个区别,当使用synchronized时,并不需要你去捕捉异常,因为线程会一直阻塞在等待锁的位置,就算你主动去thread.interrupt() synchronized并不会做任何处理。
lock.lockInterruptibly()使用时就需要你主动将此处的代码try/catch,因为在线程中断后,这里会捕捉异常并抛给你处理。这也给了你更灵活处理事物的能力。
Condition
synchronized中可以用锁对象的wait()、notify()以及notifyAll()实现等待,当然Lock也可以实现,不过子类实现相对应的Condition。
相对应的Condition里面的3个方法是await()、signal()、signalAll()。与wait()调用一样,await()之前也需要获取到锁对象,也就是Lock先要lock(),直接看例子
public class LockTest {
public static void main(String[] args) {
try {
ReentrantLock reentrantLock = new ReentrantLock();
Condition condition = reentrantLock.newCondition();
new Thread(new Runnable() {
@Override
public void run() {
try {
reentrantLock.lock();
System.out.println("wait start");
condition.await();
System.out.println("wait finish");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
}).start();
Thread.sleep(1000);
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
reentrantLock.lock();
System.out.println("signal start");
condition.signal();
System.out.println("signal finish");
} finally {
reentrantLock.unlock();
}
}
});
thread.start();
Thread.sleep(1000);
thread.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
打印如下:
wait start
signal start
signal finish
wait finish
ReentrantLock的其他诸如isLocked()等方法这里就不再花篇幅说明,有兴趣的同学可以自己查看源码
总结
总结一下这篇文章的知识点:
- volatile修饰词:保证了操作的可见性,保证多线程操作时,数据从主内存拷贝的都是最新的
- Synchronized修饰词:可作用于代码块、成员方法已经静态方法上,配合锁对象的wait()、nofity()以及nofityAll()可以实现等待/通知模型。
- 作用在代码块上时需要指定锁对象
- 作用于成员方法上时锁对象为当前实例对象也就是xxx.this
- 作用与静态方法上时锁对象为当前类的class对象也就是xxx.class
- wait()、nofity()以及nofityAll()调用前需要获得锁对象。
- Lock:与Synchronized相比较用法就是需要用户手动上锁以及释放锁对象,同时多了一个lockInterruptibly()可以让用户手动中断等待线程,并捕获异常进行后续处理。配合Condition可以实现与Synchronized一样的等待/通知模型.
- 死锁:死锁指的是某一线程一直持有锁对象不释放,其他线程无法获取锁对象的过程。产生死锁一般的原因都是多个锁对象一起使用,同时相互请求锁对象导致的。
以上是个人能想到的关于锁的内容,并不保证写的完全对,如有问题可以留言给我,共同学习。