一、多线程的同步

1、为什么要引入同步机制

在多线程环境中,可能会有两个甚至更多的线程试图同时访问一个有限的资源。必须对这种潜在资源冲突进行预防。

解决方法:在线程使用一个资源时为其加锁即可。

访问资源的第一个线程为其加上锁以后,其他线程便不能再使用那个资源,除非被解锁。

2、程序实例

用一个取钱的程序例子,来说明为什么需要引入同步。在使用同步机制前,整体程序如下:

package com.demo;

public class FetchMoneyTest {

    public static void main(String[] args){
Bank bank = new Bank(); Thread t1 = new MoneyThread(bank);// 从银行取钱
Thread t2 = new MoneyThread(bank);// 从取款机取钱 t1.start();
t2.start(); }
} class Bank{
private int money = 1000; public int getMoney(int number){
if (number < 0){
return -1;
}
else if (number > money){
return -2;
}
else if (money < 0) {
return -3;
}
else{
try{
Thread.sleep(1000);
}
catch (InterruptedException e){
e.printStackTrace();
}
money -= number; System.out.println("Left Money: " + money);
return number; }
}
} class MoneyThread extends Thread{ private Bank bank; public MoneyThread(Bank bank){
this.bank = bank;
} @Override
public void run(){
System.out.println(bank.getMoney(800));
}
}

运行结果:

Left Money: 200
800
Left Money: -600
800

程序中定义了一个Bank类,其中包含了用户存储的钱(1000元),然后用两个线程进行取钱操作,可以看到尽管Bank类中的getMoney()方法对取钱数目与存款数据进行了判断,但是执行后,结果输出两个800,表明从两个线程中都成功地取出了800元钱。

  这是为什么呢?因为getMoney()方法中有一些逻辑判断,进入最后一个else语句块后,有一个简短的休眠,那么在第一个线程休眠的过程中,第二个线程也成功进入了这个else语句块(因为存款的钱还没有取走),当两个线程结束休眠后,不再进行逻辑判断而是直接将钱取走,所以两个线程都取到了800元钱,此时money为负600。

  需要注意这里并不能确定哪一个线程是第一个线程,哪一个线程是第二个线程,先后顺序是不定的。

  在getMoney()方法中加入打印语句输出剩余的钱数,可以看到输出为剩余钱数为200,-600,或-600,-600。这是不一定的,因为可能在第一次输出剩余钱数之前,另一个线程可能还没有将钱取走,也可能已经取走。

3、解决办法

解决办法:在getMoney()方法上加上关键字synchronized。即程序改动后如下:(只是加了一个关键字) 

package com.demo;

public class FetchMoneyTest {

    public static void main(String[] args){
Bank bank = new Bank(); Thread t1 = new MoneyThread(bank);// 从银行取钱
Thread t2 = new MoneyThread(bank);// 从取款机取钱 t1.start();
t2.start(); }
} class Bank{
private int money = 1000; public synchronized int getMoney(int number){
if (number < 0){
return -1;
}
else if (number > money){
return -2;
}
else if (money < 0) {
return -3;
}
else{
try{
Thread.sleep(1000);
}
catch (InterruptedException e){
e.printStackTrace();
}
money -= number; System.out.println("Left Money: " + money);
return number; }
}
} class MoneyThread extends Thread{ private Bank bank; public MoneyThread(Bank bank){
this.bank = bank;
} @Override
public void run(){
System.out.println(bank.getMoney(800));
}
}

再次运行程序,结果如下:

Left Money: 200
800
-2

表明第一次取款800元后,剩余200元,当另一个线程再去取的时候,已经不能再取钱了。即一个线程开始执行取钱的方法之后就阻止了其他线程再去执行这个方法,直到本线程结束,其他线程才有访问权利。

二、synchronized关键字详解

多线程的同步机制对资源进行加锁,使得在同一个时间,只有一个线程可以进行操作,同步用以解决多个线程同时访问时可能出现的问题。同步机制可以使用synchronized关键字实现。当synchronized关键字修饰一个方法的时候,该方法叫做同步方法。当synchronized方法执行完或发生异常时,会自动释放锁。下面通过一个例子来对synchronized关键字的用法进行解析。

1、是否使用synchronized关键字的不同

package com.demo;

public class ThreadTest {

     public static void main(String[] args){

        Example example = new Example();

        Thread t1 = new Thread1(example);
Thread t2 = new Thread1(example); t1.start();
t2.start();
}
} class Example{ public synchronized void execute(){ for (int i = 0; i < 10; ++i){
try{
Thread.sleep(500);
}
catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("Hello: " + i);
}
} } class Thread1 extends Thread{ private Example example; public Thread1(Example example){
this.example = example;
} @Override
public void run(){
example.execute();
} }

是否在execute()方法前加上synchronized关键字,这个例子程序的执行结果会有很大的不同。

加上synchronized关键字的运行结果:

Hello: 0
Hello: 1
Hello: 2
Hello: 3
Hello: 4
Hello: 5
Hello: 6
Hello: 7
Hello: 8
Hello: 9
Hello: 0
Hello: 1
Hello: 2
Hello: 3
Hello: 4
Hello: 5
Hello: 6
Hello: 7
Hello: 8
Hello: 9

不加synchronized关键字的运行结果:

Hello: 0
Hello: 0
Hello: 1
Hello: 1
Hello: 2
Hello: 2
Hello: 3
Hello: 3
Hello: 4
Hello: 4
Hello: 5
Hello: 5
Hello: 6
Hello: 6
Hello: 7
Hello: 7
Hello: 8
Hello: 8
Hello: 9
Hello: 9

结论:

如果不加synchronized关键字,则两个线程同时执行execute()方法,输出是两组并发的。

如果加上synchronized关键字,则会先输出一组0到9,然后再输出下一组,说明两个线程是顺次执行的。

2、多个方法的多线程情况

将程序改动一下,Example类中再加入一个方法execute2()。之后再写一个线程类Thread2,Thread2中的run()方法执行的是execute2()。Example类中的两个方法都是被synchronized关键字修饰的。

package com.demo;

public class ThreadTest {

     public static void main(String[] args){

        Example example = new Example();

        Thread t1 = new Thread1(example);
//Thread t2 = new Thread1(example);
Thread t2 = new Thread2(example); t1.start();
t2.start();
}
} class Example{ public synchronized void execute(){ for (int i = 0; i < 20; ++i){
try{
//Thread.sleep(500);
Thread.sleep((long) Math.random() * 1000);
}
catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("Hello: " + i);
}
} public synchronized void execute2(){ for (int i = 0; i < 20; ++i){
try{
Thread.sleep((long) Math.random() * 1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("World: " + i);
}
} } class Thread1 extends Thread{ private Example example; public Thread1(Example example){
this.example = example;
} @Override
public void run(){
example.execute();
} } class Thread2 extends Thread{ private Example example; public Thread2(Example example){
this.example = example;
} @Override
public void run(){
example.execute2();
} }

运行结果:

Hello: 0
Hello: 1
Hello: 2
Hello: 3
Hello: 4
Hello: 5
Hello: 6
Hello: 7
Hello: 8
Hello: 9
Hello: 10
Hello: 11
Hello: 12
Hello: 13
Hello: 14
Hello: 15
Hello: 16
Hello: 17
Hello: 18
Hello: 19
World: 0
World: 1
World: 2
World: 3
World: 4
World: 5
World: 6
World: 7
World: 8
World: 9
World: 10
World: 11
World: 12
World: 13
World: 14
World: 15
World: 16
World: 17
World: 18
World: 19

如果去掉synchronized关键字,则两个方法并发执行,并没有相互影响。但是如例子程序中所写,即便是两个方法:

执行结果永远是执行完一个线程的输出再执行另一个线程的。  

说明:

  如果一个对象有多个synchronized方法,某一时刻某个线程已经进入到了某个synchronized方法,那么在该方法没有执行完毕前,其他线程是无法访问该对象的任何synchronized方法的。

结论:

  当synchronized关键字修饰一个方法的时候,该方法叫做同步方法。

  Java中的每个对象都有一个锁(lock),或者叫做监视器(monitor),当一个线程访问某个对象的synchronized方法时,将该对象上锁其他任何线程都无法再去访问该对象的synchronized方法了(这里是指所有的同步方法,而不仅仅是同一个方法),直到之前的那个线程执行方法完毕后(或者是抛出了异常),才将该对象的锁释放掉,其他线程才有可能再去访问该对象的synchronized方法。

  注意这时候是给对象上锁,如果是不同的对象,则各个对象之间没有限制关系。

  尝试在代码中构造第二个线程对象时传入一个新的Example对象,则两个线程的执行之间没有什么制约关系。

3、考虑静态的同步方法

当一个synchronized关键字修饰的方法同时又被static修饰,之前说过,非静态的同步方法会将对象上锁,但是静态方法不属于对象,而是属于类,它会将这个方法所在的类的Class对象上锁一个类不管生成多少个对象,它们所对应的是同一个Class对象。

package com.demo;

public class ThreadTest {

     public static void main(String[] args){

        Example example = new Example();

        Thread t1 = new Thread1(example);

        // 此处即便传入不同的对象,静态方法同步仍然不允许多个线程同时执行
example = new Example(); Thread t2 = new Thread2(example); t1.start();
t2.start();
}
} class Example{ public synchronized static void execute(){ for (int i = 0; i < 20; ++i){
try{
//Thread.sleep(500);
Thread.sleep((long) Math.random() * 1000);
}
catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("Hello: " + i);
}
} public synchronized static void execute2(){ for (int i = 0; i < 20; ++i){
try{
Thread.sleep((long) Math.random() * 1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("World: " + i);
}
} } class Thread1 extends Thread{ private Example example; public Thread1(Example example){
this.example = example;
} @Override
public void run(){
example.execute();
} } class Thread2 extends Thread{ private Example example; public Thread2(Example example){
this.example = example;
} @Override
public void run(){
example.execute2();
} }

运行结果:

Hello: 0
Hello: 1
Hello: 2
Hello: 3
Hello: 4
Hello: 5
Hello: 6
Hello: 7
Hello: 8
Hello: 9
Hello: 10
Hello: 11
Hello: 12
Hello: 13
Hello: 14
Hello: 15
Hello: 16
Hello: 17
Hello: 18
Hello: 19
World: 0
World: 1
World: 2
World: 3
World: 4
World: 5
World: 6
World: 7
World: 8
World: 9
World: 10
World: 11
World: 12
World: 13
World: 14
World: 15
World: 16
World: 17
World: 18
World: 19

所以如果是静态方法的情况(execute()和execute2()都加上static关键字),即便是向两个线程传入不同的Example对象,这两个线程仍然是互相制约的,必须先执行完一个,再执行下一个。

结论:

  如果某个synchronized方法是static的,那么当线程访问该方法时,它锁的并不是synchronized方法所在的对象,而是synchronized方法所在的类所对应的Class对象。Java中,无论一个类有多少个对象,这些对象会对应唯一一个Class对象,因此当线程分别访问同一个类的两个对象的两个static synchronized方法时,它们的执行顺序也是顺序的,也就是说一个线程先去执行方法,执行完毕后另一个线程才开始。

4、synchronized块

synchronized块写法:

synchronized(object){      

}

表示线程在执行的时候会将object对象上锁。(注意这个对象可以是任意类的对象,也可以使用this关键字)。

package com.demo;

public class ThreadTest {

     public static void main(String[] args){

        Example example = new Example();

        Thread t1 = new Thread1(example);
Thread t2 = new Thread2(example); t1.start();
t2.start();
}
} class Example{ private Object object = new Object(); public void execute(){ synchronized(object){
for (int i = 0; i < 20; ++i){
try{
//Thread.sleep(500);
Thread.sleep((long) Math.random() * 1000);
}
catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("Hello: " + i);
}
} } public void execute2(){ synchronized(object){
for (int i = 0; i < 20; ++i){
try{
Thread.sleep((long) Math.random() * 1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("World: " + i);
}
} } } class Thread1 extends Thread{ private Example example; public Thread1(Example example){
this.example = example;
} @Override
public void run(){
example.execute();
} } class Thread2 extends Thread{ private Example example; public Thread2(Example example){
this.example = example;
} @Override
public void run(){
example.execute2();
} }

运行结果:

Hello: 0
Hello: 1
Hello: 2
Hello: 3
Hello: 4
Hello: 5
Hello: 6
Hello: 7
Hello: 8
Hello: 9
Hello: 10
Hello: 11
Hello: 12
Hello: 13
Hello: 14
Hello: 15
Hello: 16
Hello: 17
Hello: 18
Hello: 19
World: 0
World: 1
World: 2
World: 3
World: 4
World: 5
World: 6
World: 7
World: 8
World: 9
World: 10
World: 11
World: 12
World: 13
World: 14
World: 15
World: 16
World: 17
World: 18
World: 19

例子程序4所达到的效果和例子程序2的效果一样,都是使得两个线程的执行顺序进行,而不是并发进行,当一个线程执行时,将object对象锁住,另一个线程就不能执行对应的块。

synchronized方法实际上等同于用一个synchronized块包住方法中的所有语句,然后在synchronized块的括号中传入this关键字。当然,如果是静态方法,需要锁定的则是class对象。

可能一个方法中只有几行代码会涉及到线程同步问题,所以synchronized块比synchronized方法更加细粒度地控制了多个线程的访问,只有synchronized块中的内容不能同时被多个线程所访问,方法中的其他语句仍然可以同时被多个线程所访问(包括synchronized块之前的和之后的)。

  注意:被synchronized保护的数据应该是私有的

结论:

  synchronized方法是一种粗粒度的并发控制,某一时刻,只能有一个线程执行该synchronized方法;

  synchronized块则是一种细粒度的并发控制,只会将块中的代码同步,位于方法内、synchronized块之外的其他代码是可以被多个线程同时访问到的。

Java多线程(三)—— synchronized关键字详解的更多相关文章

  1. “全栈2019”Java多线程第十六章:同步synchronized关键字详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  2. Java synchronized 关键字详解

    Java synchronized 关键字详解 前置技能点 进程和线程的概念 线程创建方式 线程的状态状态转换 线程安全的概念 synchronized 关键字的几种用法 修饰非静态成员方法 sync ...

  3. Java面试题04-final关键字详解

    Java面试题04-final关键字详解 本篇博客将会讨论java中final关键字的含义,以及final用在什么地方,感觉看书总会有一些模糊,而且解释的不是很清楚,在此做个总结,以备准备面试的时候查 ...

  4. Java 多线程(六) synchronized关键字详解

    多线程的同步机制对资源进行加锁,使得在同一个时间,只有一个线程可以进行操作,同步用以解决多个线程同时访问时可能出现的问题. 同步机制可以使用synchronized关键字实现. 当synchroniz ...

  5. [java] java synchronized 关键字详解

    Java语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码.当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一 ...

  6. Java多线程:synchronized关键字和Lock

    一.synchronized synchronized关键字可以用于声明方法,也可以用来声明代码块,下面分别看一下具体的场景(摘抄自<大型网站系统与Java中间件实践>) 案例一:其中fo ...

  7. 从线程池到synchronized关键字详解

    线程池 BlockingQueue synchronized volatile 前段时间看了一篇关于"一名3年工作经验的程序员应该具备的技能"文章,倍受打击.很多熟悉而又陌生的知识 ...

  8. Java并发之Synchronized机制详解

    带着问题阅读 1.Synchronized如何使用,加锁的粒度分别是什么 2.Synchronized的实现机制是什么 3.Synchronized是公平锁吗 4.Java对Synchronized做 ...

  9. Java多线程同步 synchronized 关键字的使用

    代表这个方法加锁,相当于不管哪一个线程A每次运行到这个方法时,都要检查有没有其它正在用这个方法的线程B(或者C D等),有的话要等正在使用这个方法的线程B(或者C D)运行完这个方法后再运行此线程A, ...

随机推荐

  1. Building QGIS from source - step by step (开发文档翻译1)

    1. 简介 原文网址:http://htmlpreview.github.io/?https://raw.github.com/qgis/QGIS/master/doc/INSTALL.html 本文 ...

  2. 解决ie6中png图片格式不兼容问题

    在IE6中对图片格式png24支持度不高,如果使用的图片格式是png24,则会导致透明效果无法正常显示 解决方法: 1.可以使用png8来代替png24,即可解决问题,但是使用png8代替png24以 ...

  3. python+selenium+PhantomJS爬取网页动态加载内容

    一般我们使用python的第三方库requests及框架scrapy来爬取网上的资源,但是设计javascript渲染的页面却不能抓取,此时,我们使用web自动化测试化工具Selenium+无界面浏览 ...

  4. svn checkout 实用小技巧

    svn checkout 实用小技巧 by:授客 QQ:1033553122   问题描述: 用svn小乌龟软件,进行update,commit之前,先要把svn工作目录checkout到本地,那么问 ...

  5. ionic开发中,输入法键盘弹出遮挡住div元素

    采用ionic 开发中,遇到键盘弹出遮挡元素的问题. 以登陆页面为例,输入用户名和密码时,键盘遮挡了登陆按钮. 最终采用自定义指令解决了问题: .directive('popupKeyBoardSho ...

  6. Pycharm基本设置和插件安装

    Pycharm调节主题和字体 调节主题:File - Setting - Editor - Color Scheme - 选择个人喜欢的风格 调节字体大小,感觉默认字体有点小,对我这样的老人家,至少要 ...

  7. JVM 之类加载

    一.概述 Java不同于C/C++这类传统的编译型语言,也不同于php这一类动态的脚本语言.可以说Java是一种半编译语言,我们所写的类会先被编译成.class文件,这个.class是一串二进制的字节 ...

  8. Scala高阶函数实践

    高阶函数主要有两种:一种是将一个函数当做另外一个函数的参数(即函数参数):另外一种是返回值是函数的函数.package sparkCore/** * Created by zhen on 2018/3 ...

  9. stored information about method csdn

    Eclipse编译时保留方法的形参 Window -> Preferences -> Java -> Compiler. 选中Store information about meth ...

  10. 常用判断重复记录的SQL语句

    1.查找表中多余的重复记录,重复记录是根据单个字段(peopleId)来判断select * from people where peopleId in (select   peopleId  fro ...