【Java多线程与并发库】03 传统线程互斥技术 synchronized

时间:2021-04-20 17:32:23

【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讲义》