【Java多线程与并发库】03 传统线程互斥技术 synchronized
我们会使用模拟打印机的程序来说明线程的互斥技术,如下。
打印机出了问题
现在我们写一个模拟打印机打印文本的程序。
打印机的打印方法接收到一个字符串,它会依次打印字符串中的每一个字符。
程序如下:
/**
* 模拟打印机
*/
class Outputer{
public void output(String content){
for(int i = 0; i < content.length(); i ++){
System.out.print(content.charAt(i));
}
System.out.println();
}
}
之后,我们启动两个线程类执行打印任务,这两个线程共享同一个打印机。
其中第一个线程不停地打印”llllllll”,第二个线程不停地打印”oooooooo”,我们期望两者各自工作,互不干扰。
private void init(){
Outputer outputer = new Outputer(); // 两个线程共享同一个打印机
new Thread(new Runnable() {
@Override
public void run() {
while(true) {
outputer.output("llllllll");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while(true) {
outputer.output("oooooooo");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
两个线程共享同一个打印机执行任务的结果如下:
llllllll
oooooooo
ooollllllll
ooooo
llllllooooooll
oo
lllllloooooooo
ll
ooolllooooo
lllll
llllllll
oooooooo
可见,出现了我们不期望的结果,一个字符串没有打印完就开始打印另外一个字符串了。
为什么会出现这种“不正确”的情况?
因为是线程调度的不确定性。
当一个线程还没有完成当前打印任务时,它的时间片被用完了,那么就会执行另一个线程,所以就出现了上面的不期望的情况。
怎么才能保证一个线程在执行打印任务时,不被其他的线程干扰呢?
……
是不是满足下面这个情况就可以呢?
当一个线程在执行打印任务(打印任务没有结束)时,其他线程就不能够再执行打印任务。
是的。
那怎么做才可以达到这个目的呢?
这就要用到线程的互斥技术,Java提供了synchronized关键字来达到线程互斥目的。
这里我们仅仅讨论synchronized关键字的作用和用法,关于它的原理,可以参看其他文章。
synchronized 关键字
synchronized,字面意思是:同步的,同步化的。
为了解决上面的问题,Java多线程引入了同步监视器。使用同步监视器的通用方法就是同步代码块,同步代码块的语法格式如下:
synchronized(obj){
// 此处是同步的代码
}
上面语法格式中的 obj 就是同步监视器。
同步代码块的含义是:
线程开始执行同步带块之前,必须先获得同步监视器的锁定。
同步代码块执行结束后,同步监视器会被自动地释放。
同步监视器的目的是,阻止两个线程对同一个共享资源进行并发访问,因此通常使用可能被并发访问的资源充当同步监视器。
此处,打印机是并发访问的资源。
当使用synchronized修饰一个方法时, 同步监视器是 this,也即是调用当前方法的对象,不再需要指明同步监视器对象。
更直白的解释是:
当使用synchronized修饰一个方法或者代码块时,这部分代码就变成为同步的,它不会同时被两个线程同时执行。
也就是,当一个线程正在执行该代码时,其他的线程也要执行这部分代码时,synchronized会将第二个线程阻止在外,当第一个线程执行结束后,才会让第二个线程执行。
由此,我们可以完善之前的打印机的打印方法,public synchronized void outputSafely(String content)如下:
/**
* 模拟打印机
*/
class Outputer{
public void output(String content){
for(int i = 0; i < content.length(); i ++){
System.out.print(content.charAt(i));
}
System.out.println();
}
public synchronized void outputSafely(String content){
for(int i = 0; i < content.length(); i ++){
System.out.print(content.charAt(i));
}
System.out.println();
}
}
此时的执行结果如下
llllllll
oooooooo
oooooooo
llllllll
oooooooo
llllllll
oooooooo
llllllll
oooooooo
llllllll
oooooooo
llllllll
llllllll
oooooooo
这就是我们期望的打印机执行的结果。
释放同步监视器的锁定
任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢?
程序无法显示释放对同步监视器的锁定。
线程会在如下几种情况下释放对同步监视器的锁定。
- 当前线程的同步方法、同步代码块执行结束,当前线程会释放同步监视器。
- 当前线程在同步方法、同步代码块中出现了未处理的Error或Exception,导致了该方法、代码块异常结束时,当前线程将会释放同步监视器。
- 当前线程在同步方法、同步代码块中调用了同步监视器对象的 wait()方法,则当前线程暂停,并释放同步监视器。
当前线程在同步方法、同步代码块中调用了Thread.sleep() 或者 Thread.yield()方法暂停当前线程的执行,当前线程不会释放同步监视器。
附本文章中的完整程序
/**
* description:
*
* @author liyazhou
* @since 2017-08-14 14:31
*/
public class TraditionalThreadSynchronized {
/**
* 模拟打印机
*/
class Outputer{
public void output(String content){
for(int i = 0; i < content.length(); i ++){
System.out.print(content.charAt(i));
}
System.out.println();
}
}
private void init(){
Outputer outputer = new Outputer();
new Thread(new Runnable() {
@Override
public void run() {
while(true) {
outputer.output("llllllll");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while(true) {
outputer.output("oooooooo");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
public static void main(String[] args){
new TraditionalThreadSynchronized().init();
}
}
参考
《Java多线程与并发库高级应用》
《疯狂Java讲义》