Java多线程(三)—— synchronized关键字详解
一、多线程的同步
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关键字详解的更多相关文章
- “全栈2019”Java多线程第十六章:同步synchronized关键字详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...
- Java synchronized 关键字详解
Java synchronized 关键字详解 前置技能点 进程和线程的概念 线程创建方式 线程的状态状态转换 线程安全的概念 synchronized 关键字的几种用法 修饰非静态成员方法 sync ...
- Java面试题04-final关键字详解
Java面试题04-final关键字详解 本篇博客将会讨论java中final关键字的含义,以及final用在什么地方,感觉看书总会有一些模糊,而且解释的不是很清楚,在此做个总结,以备准备面试的时候查 ...
- Java 多线程(六) synchronized关键字详解
多线程的同步机制对资源进行加锁,使得在同一个时间,只有一个线程可以进行操作,同步用以解决多个线程同时访问时可能出现的问题. 同步机制可以使用synchronized关键字实现. 当synchroniz ...
- [java] java synchronized 关键字详解
Java语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码.当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一 ...
- Java多线程:synchronized关键字和Lock
一.synchronized synchronized关键字可以用于声明方法,也可以用来声明代码块,下面分别看一下具体的场景(摘抄自<大型网站系统与Java中间件实践>) 案例一:其中fo ...
- 从线程池到synchronized关键字详解
线程池 BlockingQueue synchronized volatile 前段时间看了一篇关于"一名3年工作经验的程序员应该具备的技能"文章,倍受打击.很多熟悉而又陌生的知识 ...
- Java并发之Synchronized机制详解
带着问题阅读 1.Synchronized如何使用,加锁的粒度分别是什么 2.Synchronized的实现机制是什么 3.Synchronized是公平锁吗 4.Java对Synchronized做 ...
- Java多线程同步 synchronized 关键字的使用
代表这个方法加锁,相当于不管哪一个线程A每次运行到这个方法时,都要检查有没有其它正在用这个方法的线程B(或者C D等),有的话要等正在使用这个方法的线程B(或者C D)运行完这个方法后再运行此线程A, ...
随机推荐
- Statement和PreparedStatement的异同
1.首先两个都是java向数据库执行sql语句的对象! java代码连接数据库,并且执行sql语句的步骤如下: //1.注册数据库的驱动程序 Class.forName(driverClass); / ...
- pyinstaller使用错误 SyntaxError: Non-UTF-8 code starting with '\xb4' in file C:......
注:我的博客原本在CSDN,现转到博客园,图片采用以前的图片,并没有盗图. 在将.py文件打包时,出现了下列错误 >>C:\Users\小呆\PycharmProjects\pycha ...
- MyBatis-Plus初步使用
在使用mybatis的过程中,我们会发现需要自己写很多的mapper和mapper.xml配置文件,很多时候会写到相当多的重复代码,特别是普通的增删改查,这样不仅会影响我们的开发效率,也会使得代码变的 ...
- (网页)websocket例子
转载自博客园张果package action; import javax.websocket.CloseReason; import javax.websocket.OnClose; import j ...
- 作为IT,你的价值在哪里?
也许最近是真的被无穷无尽的数据整理.导入.再整理.再导入给恶心到了. 业务部提交的数据只是一个非常初始的数据,IT还得在这个基础上七整八整,对导出的结果还要再做二次导入三次导入,不仅要帮业务部批导生成 ...
- 读JP摩根的《加密货币展望》阅读笔记
加密货币不可能死掉, 非常容易以各种形式生存下去早期加密货币的半数以上交易额是地下钱庄交易. 现在已经被投资和投机交易取代.加密货币不可能取代政府发行的货币,其影响到了美元人民币的主权利益加密货币面临 ...
- MySQL 基本语句(1)
一.cmd命令行的常用命令: 当我们使用MySQL 5.5 Command Line Client这个客户端登陆时,只能登陆root用户.如果今后创建了别的用户,就很麻烦了,所以我们不用MySQL 5 ...
- (面试题)python面试题集锦-附答案
1.一行代码实现1-100的和 sum_1_100 = sum(range(1, 101)) 2.如何在一个函数内修改全局变量的值 a = 100 def foo(): global a a = 30 ...
- LeetCode题解之Binary Tree Right Side View
1.题目描述 2.问题分析 使用层序遍历 3.代码 vector<int> v; vector<int> rightSideView(TreeNode* root) { if ...
- The JSP specification requires that an attribute name is
把另一个博客内容迁移到这里 七月 10, 2016 10:23:12 上午 org.apache.catalina.core.ApplicationDispatcher invoke 严重: Serv ...