一、多线程的同步

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. 在Arcmap中加载互联网地图资源的4种方法

    前一段时间想在Arcmap中打开互联网地图中的地图数据,如影像数据.基础地图数据等,经过简单研究目前总结了四种方法,整理下与大家分享,有些内容可能理解有误,希望大家多多指教.4种方法如下: a)    ...

  2. Openlayer3之绚丽的界面框架-Materialize

    一群做C++的老伙计搞前端开发,徒手写html和css应该会折寿..在网上找了半天,Materialize算是用起来很方便的一款前端界面框架.Google的Material Design看起来感觉还是 ...

  3. neutron 多租户隔离的实现以及子网间路由的实现

    1.一个network相当于一个二层网络,使用vxlan 隧道连通所有的CNA节点. 2.一个VPC下有多个network,也就是会分配多个vxlan隧道,这些子网间的路由是通过DVR实现的.DVR就 ...

  4. iOS------Xcode 的clang 扫描器可以检测出所有的内存泄露吗

    在苹果没有出ARC(自动内存管理机制)时,我们几乎有一半的开发时间都耗费在这么管理内存上.后来苹果很人性的出了ARC,虽然在很大程度上,帮助我们开发者节省了精力和时间.但是我们在开发过程中,由于种种原 ...

  5. ES搜索引擎基本操作

    一.创建索引库 执行:,索引库的名称为zhen 结果(我已经创建过了,重复执行会报错): 二.创建索引 1.添加索引 2.添加索引(动态添加新列) 3.添加指定id的索引[根据id可以直接修改一前的索 ...

  6. SQL 删除外键列

    一 SQL删除列的语句是: alter table tableName drop column columnName --(其中,tableName为表名,columnName为列名) 但是,如果某列 ...

  7. org.hibernate.NonUniqueObjectException

    错误如下: 2017-3-29 15:17:52~ERROR~org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperV ...

  8. 第10章 嵌入式Linux 的调试技术

    10.1  打印内核调试信息:printk printk位函数运行在内核空间, printf函数运行在用户空间.也就是说,像Linux 驱动这样的Linux内核程序只能使用printk函数输出调试信息 ...

  9. Ubuntu16.04系统安装搜狗输入法详细教程(转载)

    1.下载搜狗输入法的安装包 下载地址为:http://pinyin.sogou.com/linux/ ,如下图,要选择与自己系统位数一致的安装包,我的系统是64位,所以我下载64位的安装包 2.按键C ...

  10. sci-hub免费下载论文

    作者:凯鲁嘎吉 - 博客园 http://www.cnblogs.com/kailugaji/ sci-hub网址: https://gfsoso.99lb.net/sci-hub.html 免费下载 ...