一、多线程的同步

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. ICML 2018 | 从强化学习到生成模型:40篇值得一读的论文

    https://blog.csdn.net/y80gDg1/article/details/81463731 感谢阅读腾讯AI Lab微信号第34篇文章.当地时间 7 月 10-15 日,第 35 届 ...

  2. ios12怎么投屏电脑 苹果手机怎么投

    Ios12系统发布成功之后,是不是给我们带来更大的惊喜呢.我们只需要利用手机上的屏幕镜像就可以轻松将手机画面投屏至电脑上,那么ios12怎么投屏电脑?下面便是今天所要分享的手机投屏的方法. 使用工具: ...

  3. 腾讯.NET&PHP面试题

    在整个面试过程中,作为面试者的你,角色就是小怪兽,面试官的角色则是奥特曼,更不幸的是,作为小怪兽的你是孤身一人,而奥特曼却往往有好几个助攻,你总是被虐得不要不要的~ 作为复读一年才考上专科的我,遗憾的 ...

  4. 2018-05-27-computer-using-hints-电脑使用帮助[持续更新]

    layout: post title: 2018-05-27-computer-using-hints-电脑使用帮助 key: 20180527 tags: ubuntu cuda cudnn ten ...

  5. Greenplum启动失败Error occurred: non-zero rc: 1的修复

    某日开发反馈测试环境的集群启动失败 报错内容如下: [gpadmin@hadoop-test2:/root]$ gpstart :::: gpstart:hadoop-test2:gpadmin-[I ...

  6. Android内嵌VLC实现播放网络视频,网络音频

    1.在对应模块的build.gradle文件中,添加依赖 //VlC implementation "de.mrmaffen:vlc-android-sdk:2.0.6" 2.布局 ...

  7. c/c++ 线性表之单向链表

    c/c++ 线性表之单向链表 线性表之单向链表 不是存放在连续的内存空间,链表中的每个节点的next都指向下一个节点,最后一个节点的下一个节点是NULL. 真实的第一个节点是头节点,头节点不存放数据, ...

  8. Java入门(二):注释和基本数据类型

    上次通过eclipse在控制台输出了hello world,是不是有点小激动啊,今天接着介绍Java基础知识. 一.Java注释 1.Java注释语句不会被编译器运行,不用担心代码因为许多注释语句显得 ...

  9. Teradata 终止回滚方法(rcvmanager工具)

    1.使用root用户登录数据库节点 ssh root 2.启动database window cnsterm 3.启动rcvmanager start rcvmanager 4.确认utiltiy在哪 ...

  10. Apache Curator is a Java/JVM client library for Apache ZooKeeper

    http://curator.apache.org/index.html Welcome to Apache Curator What is Curator? Curator n ˈkyoor͝ˌāt ...