Java 多线程(三)——线程同步
一、提出问题
通过 2 个线程,来实现 a、b 的随机逐行打印,每行 10 个字母逐字打印。
效果类似如下:
aaaaaaaaaa
aaaaaaaaaa
bbbbbbbbbb
bbbbbbbbbb
aaaaaaaaaa
aaaaaaaaaa
aaaaaaaaaa
aaaaaaaaaa
bbbbbbbbbb
bbbbbbbbbb
二、错误案例源码
代码:
我们先写一个打印实现类 Out
public class Out { public void print(String str) { for (int i = 0; i < str.length(); i++) { System.out.print(str.charAt(i)); } System.out.println(); } }
写一个 main 方法,通过 2 个线程来调用 Out 的 print 方法
public static void main(String[] args) { final Out out = new Out(); Thread t1 = new Thread(new Runnable(){ @Override public void run() { while (true) { out.print("aaaaaaaaaa"); } } }); Thread t2 = new Thread(new Runnable(){ @Override public void run() { while (true) { out.print("bbbbbbbbbb"); } } }); t1.start(); t2.start(); }
结果:
代码看起来没问题,但运行起来却发现
bbbbbbbbbb
aaaaaaaaa
aaaaaaaaaa
bbaaabbbbbbaaabb
bbbbabbbbbb
原因:
在java中所以的线程都是同时启动的,至于什么时候,哪个先执行,看谁先得到CPU的资源。
说白了,就是CPU对线程执行的随机调度,比如上面的 t1 线程此时正在打印信息还没打印完毕此时 CPU 切换到 t2 线程执行了,t2 线程执行完了又切换回 t1 线程执行就会导致上面现象发生。
解决办法
解决办法就是把多个线程要调用的同一方法或者操作的同一变量加上一把锁,只有得到了这个锁,才能进行调用。
具体方法有以下几种:
- 同步方法
- 同步代码块
- 静态函数的同步
- Lock锁机制
三、同步方法
其实就是个方法加上 synchronized 关键字。
代码:
public class Out {
public synchronized void print(String str) {
for (int i = 0; i < str.length(); i++) {
System.out.print(str.charAt(i));
}
System.out.println();
}
}
结果:
aaaaaaaaaa
aaaaaaaaaa
bbbbbbbbbb
bbbbbbbbbb
bbbbbbbbbb
aaaaaaaaaa
原因:
为 print 方法加上 synchronized 后其就变成了同步方法,普通同步方法的锁是 this,也就是当前对象,比如案例中,外部要想调用 print 方法就必须创建 Out 类实例对象 out,此时 print 同步方法的锁就是这个 out。
四、同步代码块
其实 synchronized 还可以用在代码快上。
代码:
首先,我们为 Out 类添加 print1 方法
public void print1(String str) { synchronized (str) { for (int i = 0; i < str.length(); i++) { System.out.print(str.charAt(i)); } System.out.println(); } }
让 t2 线程调用 print1 方法
public static void main(String[] args) { final Out out = new Out(); Thread t1 = new Thread(new Runnable(){ @Override public void run() { while (true) { out.print("aaaaaaaaaa"); } } }); Thread t2 = new Thread(new Runnable(){ @Override public void run() { while (true) { out.print1("bbbbbbbbbb"); } } }); t1.start(); t2.start(); }
结果:
但是一运行,就又出问题了。
bbbbbbbbbb
bbbbbbaaaaaaaa
aaaaaaaaaa
aaaaaaaaaa
bbbb
原因:
同步代码块的写法是 synchronized(obj){},其中 obj 为锁对象。
因为 print 与 print1 方法的锁不一样导致的,线程 t1 调用 print 方法拿到 this 这把锁,线程 t2 调用 print1 拿到 str 这把锁,二者互不影响。
解决:
其实解决办法很简单,修改 print1 方法,使其锁对象为 this。
public void print1(String str) {
synchronized (this) {
for (int i = 0; i < str.length(); i++) {
System.out.print(str.charAt(i));
}
System.out.println();
}
}
五、静态函数的同步
复制 print 方法,命名为 print2,并为其添加 static 关键字,使其为静态方法。
代码:
print2 方法
public synchronized static void print2(String str) { for (int i = 0; i < str.length(); i++) { System.out.print(str.charAt(i)); } System.out.println(); }
让 2 个线程分别调用 print1、print2 方法
public static void main(String[] args) { final Out out = new Out(); Thread t1 = new Thread(new Runnable(){ @Override public void run() { while (true) { out.print1("aaaaaaaaaa"); } } }); Thread t2 = new Thread(new Runnable(){ @Override public void run() { while (true) { Out.print2("bbbbbbbbbb"); } } }); t1.start(); t2.start(); }
结果:
但是一运行,就又出问题了。
aaaaaaaaaa
aaaaaabbbbbbbbbb
bbbbbbbbbb
aaaa
原因:
其实这里的原因更上一个很相像,因为 print2 方法是静态方法,静态方法是没有对应类的实例对象的,所以它的锁并不是 this,它的锁是 类的字节码对象。并且类的字节码对象是优先于类实例对象存在的。
解决:
解决办法就是调整 print1 方法的锁对象
public void print1(String str) {
synchronized (Out.class) {
for (int i = 0; i < str.length(); i++) {
System.out.print(str.charAt(i));
}
System.out.println();
}
}
六、Lock锁机制
从 JDK1.5 起我们就可以根据需要显性的获取锁以及释放锁了,这样也更加符合面向对象原则。
Lock 接口的实现子类之一 ReentrantLock,翻译过来就是重入锁,就是支持重新进入的锁,该锁能够支持一个线程对资源的重复加锁,也就是说在调用 lock() 方法时,已经获取到锁的线程,能够再次调用 lock() 方法获取锁而不被阻塞。
同时还支持获取锁的公平性和非公平性,所谓公平性就是多个线程发起 lock() 请求,先发起的线程优先获取执行权,非公平性就是获取锁与是否优先发起 lock() 操作无关。
默认情况下是不公平的锁。因为保证公平就必须需要额外开销,这样性能就下降了,所以公平与性能是有一定矛盾的,除非公平策略对你的程序很重要,比如必须按照顺序执行线程,否则还是使用不公平锁为好。
代码:
添加一个 print3 方法
private Lock lock = new ReentrantLock();
// private Lock lock = new ReentrantLock(true);
public synchronized void print3(String str) {
lock.lock(); // 如果有其它线程已经获取锁,那么当前线程在此等待直到其它线程释放锁。
try {
for (int i = 0; i < str.length(); i++) {
System.out.print(str.charAt(i));
}
System.out.println();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock(); // 释放锁资源
// 使用 try{}catch{}finally{},是为了保证 锁资源的释放
// 防止因为代码的异常而 死锁
}
}
七、总结
普通同步函数的锁是 this,当前类实例对象,同步代码块锁可以自己定义,静态同步函数的锁是类的字节码文件,不同锁之间互不影响。
synchronized:
- synchronized 是在 JVM 层面上实现的(所有对象都自动含有单一的锁。
- synchronized 在锁定时如果方法块抛出异常,JVM 会自动将锁释放掉。
- 在资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized 是很合适的。原因在于,编译程序通常会尽可能的进行优化synchronize,另外可读性非常好。
Lock:
- Lock 的锁定是通过代码实现的。
- Lock 出现异常时必须在 finally 将锁释放掉,否则将会引起死锁。
- 在资源竞争激烈情况下,Lock同步机制性能会更好一些。