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

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值