2.3多线程(java学习笔记)synchronized关键字
一、为什么要用synchronized关键字
首先多线程中多个线程运行面临共享数据同步的问题。
多线程正常使用共享数据时需要经过以下步骤:
1.线程A从共享数据区中复制出数据副本,然后处理。
2.线程A将处理好的数据副本写入共享数据区。
3.线程B从共享数据区中复制出数据副本。
如此循环,直到线程结束。

假如线程A从共享数据区中复制出数据副本然后处理,在还没有将更新的数据放入主内存时,线程B来到主内存读取了未更新的数据,这样就出问题了。
这就是所谓的脏读,这类问题称为多线程的并发问题。
举个具体的例子:
public class TestThread {
public static void main(String[] args){
TestSynchronized s = new TestSynchronized();
new Thread(s,"t1").start(); //两个线程访问一个对象
new Thread(s,"t2").start();
}
}
class TestSynchronized implements Runnable{
private int ticket = 5;
public void run(){
for(int p = 0; p < 10; p++){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(ticket >= 0){
System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
}
}
}
}
运行结果:
t2 ticket:4
t1 ticket:5
t1 ticket:2
t2 ticket:3
t1 ticket:1
t2 ticket:1
t2 ticket:0
可以看到1号票同时给了t1和t2,当t1读入1执行了ticket--后,数据还没有来得及写入主内存就被t2从主内存中读走了1,就造成了这种现象。
要想避免这种现象就需要使用synchronized关键字,synchronized英译为同步,我们认为暂且把他看做锁定更好理解。
接下来我们看看synchronized如何使用。
二、synchronized的用法
1. synchronized修饰方法(也称同步方法)
(1) java中每个对象都有一个锁(lock),或者叫做监视器,当前线程访问某个对象中synchronized修饰的方法(同步块)时,线程需要获取到该对象的锁,获取对象锁后才能访问该对象中synchronized方法(同步块),且一个对象中只有一个锁。
(2) 没有获得该对象的锁的其他线程,无法访问该对象中synchronized修饰的方法(同步块)。
(3) 其他线程要想访问该对象中synchronized修饰的方法需要获取该对象的锁。
(4) 对象锁只有将synchronized方法(同步块)中的内容运行完毕或遇到异常才会释放锁。
例一:
public class TestThread {
public static void main(String[] args){
TestSynchronized s = new TestSynchronized();
new Thread(s,"t1").start(); //两个线程访问一个对象
new Thread(s,"t2").start();
}
}
class TestSynchronized implements Runnable{
private int ticket = 5;
synchronized public void run(){
for(int p = 0; p < 10; p++){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(ticket >= 0){
System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
}
}
}
}
运行结果:
t1 ticket:5
t1 ticket:4
t1 ticket:3
t1 ticket:2
t1 ticket:1
t1 ticket:0
我们来分析上面程序,首先线程t1进去run方法获得对象s的锁,然后执行完run方法释放锁,run运行忘了也就没有t2的事了。
因为只有将synchronized修饰的方法执行完才会释放锁,故打印五个t1.。
还有一点,如果一个对象里面有多个synchronized方法,某一时刻只能有一个线程进入其中一个synchronized修饰的方法,则这时其他任何线程无法进入该对象中任何一个synchronized修饰的方法。
补充片段:
public class TestThread {
public static void main(String[] args){
Test m1 = new Test(); //两个线程共访问一个对象。
TestSynchronized_1 s1 = new TestSynchronized_1(m1);
TestSynchronized_2 s2 = new TestSynchronized_2(m1);
new Thread(s1,"t1").start();
new Thread(s2,"t2").start();
}
}
class Test{
synchronized public void test1(){
for(int p = 0; p < 5; p++){
System.out.println("s1.run.TestSynchronized_test 1");
}
}
synchronized public void test2(){
for(int p = 0; p < 5; p++){
System.out.println("s2.run.TestSynchronized_test 2");
}
}
}
class TestSynchronized_1 implements Runnable{
private Test m;
public TestSynchronized_1(Test m){
this.m = m;
}
public void run(){
m.test1();
}
}
class TestSynchronized_2 implements Runnable{
private Test m;
public TestSynchronized_2(Test m){
this.m = m;
}
public void run(){
m.test2();
}
}
运行结果:
s1.run.TestSynchronized_test 1
s1.run.TestSynchronized_test 1
s1.run.TestSynchronized_test 1
s1.run.TestSynchronized_test 1
s1.run.TestSynchronized_test 1
s2.run.TestSynchronized_test 2
s2.run.TestSynchronized_test 2
s2.run.TestSynchronized_test 2
s2.run.TestSynchronized_test 2
s2.run.TestSynchronized_test 2
当线程t1运行synchronized修饰的test1方法时,线程t2是无法运行test2方法。结合之前说的,一个对象锁只有一把,而这里是两个线程共享对象(m1),当线程t1获得锁时,线程t2就只能等待。归根结底把握几个要点:
1.锁的唯一性(一个对象只有一把锁,但不同对象就有不同的锁)
2.没锁不能进去入synchronized修饰内容中运行。
3.只有运行完synchronized修饰的内容或遇到异常才释放锁。
我们来看下面这个代码:
例二:
public class TestThread {
public static void main(String[] args){
Mouth m1 = new Mouth();
Mouth m2 = new Mouth();
TestSynchronized s1 = new TestSynchronized(m1);//两个线程访问两个对象。
TestSynchronized s2 = new TestSynchronized(m2);
new Thread(s1,"t1").start(); //线程t1
new Thread(s2,"t2").start(); //线程t2
}
}
class Mouth{ //资源及方法
synchronized public void test(){
int ticket = 5;
for(int p = 0; p < 10; p++){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(ticket >= 0){
System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
}
}
}
}
class TestSynchronized implements Runnable{
private Mouth m = new Mouth();
public TestSynchronized(Mouth m){
this.m = m;
}
synchronized public void run(){
m.test();
}
}
运行结果:
t1 ticket:5
t2 ticket:5
t2 ticket:4
t1 ticket:4
t1 ticket:3
t2 ticket:3
t2 ticket:2
t1 ticket:2
t1 ticket:1
t2 ticket:1
t2 ticket:0
t1 ticket:0
可以发现好像用synchronized修饰的test方法没有起作用,怎么是t1,t2怎么是交替运行的?
我们回顾下之前说的对象锁,线程获得对象锁后可以访问该对象里面synchronized修饰的方法,其他线程无法访问。
我们上面的代码里面对象有两个,一个是m1、一个是m2。
t1获得了对象m1的锁,然后访问m1中的test方法;t2获得了对象m2的锁,然后访问s2中的test方法。
线程t1和线程t2访问的是不同的资源(m1,m2),并不相互干扰所以没有影响。例一中是因为两个线程访问同一个资源(s1)所以synchronized的起了限制作用。
synchronized修饰方法时只能对多个线程访问同一资源(对象)时起限制作用。
可能大家会说了,那我们有没有办法也限制下这种情况呢,答案当然是可以的。
这就是下面要说的:
2.synchronized修饰静态方法
当修饰静态方法时锁定的是类,而不是对象,我们先把例二修改下看下结果。
例三:
public class TestThread {
public static void main(String[] args){
Mouth m1 = new Mouth();
Mouth m2 = new Mouth();
TestSynchronized s1 = new TestSynchronized(m1);
TestSynchronized s2 = new TestSynchronized(m2); //两个线程访问两个对象
new Thread(s1,"t1").start();
new Thread(s2,"t2").start();
}
}
class Mouth{
synchronized public static void test(){ //改为静态方法,锁定的是类。
int ticket = 5;
for(int p = 0; p < 10; p++){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(ticket >= 0){
System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
}
}
}
}
class TestSynchronized implements Runnable{
private Mouth m = new Mouth();
public TestSynchronized(Mouth m){
this.m = m;
}
synchronized public void run(){
m.test();
}
}
运行结果:
t1 ticket:5
t1 ticket:4
t1 ticket:3
t1 ticket:2
t1 ticket:1
t1 ticket:0
t2 ticket:5
t2 ticket:4
t2 ticket:3
t2 ticket:2
t2 ticket:1
t2 ticket:0
当synchronized修饰静态方法时,线程需要获得类(Mouth)锁才能运行,没有获得类锁的线程无法运行,且获得类锁的线程会将synchronized修饰的静态方法会运行完毕才释放类锁。
例如例三中的代码,t1先获得类(Mouth)锁运行Mouth类中的test方法,而t2没有类(Mouth)锁就无法运行。一个类只有一个类锁,却可以有多个对象(t1,t2等...)都是一个类(Mouth)中的对象,只要一个线程获取了类(Mouth)锁,其他线程就要等到类锁被释放,然后获得类(Mouth)锁之后才能运行类(Mouth)中synchronized修饰的静态方法。所以即使是两个线程(t1,t2)访问两个不同的资源(m1,m2)也会受到限制,因为m1,m2都属于一个类(Mouth),而锁住类(Mouth)后每次只能有一个线程访问该类(Mouth)中的sychronized修饰的静态方法。
当t1访问m1中的test时,首先获得类(Mouth)锁,这时如果t2访问m2中的test方法时也需要获得类锁,可是这时类锁已经被线程t1获得,故t2无法访问m2中的方法。只有等t1运行完方法中的内容或异常释放锁后t2才有机会获得锁,获得锁后才能运行。
而之前例一中t1,t2锁的是对象,需要结合这几段代码理解下。
3.synchronized块(也称同步块)
如果每次都锁定的范围都是一个方法,每次只能有一个线程进去势必会导致效率的低下,这主要是锁定范围过多引起的。
这时可以根据实际情况锁定合适的区域,这就要用到同步块了
synchronized(需要锁住的对象或类){
锁定的部分,需要锁才能运行。
}
()中可以确定锁定的是对象还是类,锁定对象的话可以用this,对类上锁类名加class,例如要锁定Mounth类(Moutn.class)。
我们首先看个没有任何同步的例子:
public class TestThread {
public static void main(String[] args){
TestSynchronized s1 = new TestSynchronized();
new Thread(s1,"t1").start(); //两个线程访问一个对象
new Thread(s1,"t2").start();
}
}
class TestSynchronized implements Runnable{
private int ticket = 5;
public void run(){
for(int p = 0; p < 10; p++){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(ticket >= 0){
System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
}
}
}
}
运行结果:
t1 ticket:5
t2 ticket:4
t2 ticket:3
t1 ticket:2
t2 ticket:1
t1 ticket:1
t2 ticket:0
t1 ticket:-1
其中出现了-1,我们在其中一块区域加上同步形成同步块。
public class TestThread {
public static void main(String[] args){
TestSynchronized s1 = new TestSynchronized(); //两个线程访问一个对象
new Thread(s1,"t1").start();
new Thread(s1,"t2").start();
}
}
class TestSynchronized implements Runnable{
private int ticket = 5;
public void run(){
for(int p = 0; p < 10; p++){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized(this){ //此次加上同步块,这部分内容一次只有一个线程可以进入,其他内容不受约束。
if(ticket >= 0){ //这里锁的是对象,这里面的内容需要对象锁才能运行。
System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
}
}
}
}
}
运行结果:
t2 ticket:5
t1 ticket:4
t1 ticket:3
t2 ticket:2
t1 ticket:1
t2 ticket:0
一次只能有一个线程进入同步块中,就不会出现线程读了未更新的数据或者多减一次的情况。未加synchronized修饰的其他区域不受影响,故两个线程的顺序不定。
下面我们来看一个同步块锁定类的例子,效果和例三一样,只不过例三使用静态方法锁定了类,而下面这个是使用同步块锁定了类。
public class TestThread {
public static void main(String[] args){
TestSynchronized s1 = new TestSynchronized();
TestSynchronized s2 = new TestSynchronized(); //两个线程访问两个对象
new Thread(s1,"t1").start();
new Thread(s2,"t2").start();
}
}
class TestSynchronized implements Runnable{
private int ticket = 5;
synchronized public void run(){
synchronized(TestSynchronized.class){ //将synchronized修饰的静态方法改成了同步块。
for(int p = 0; p < 10; p++){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(ticket >= 0){
System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
}
}
}
}
}
运行结果:
t1 ticket:5
t1 ticket:4
t1 ticket:3
t1 ticket:2
t1 ticket:1
t1 ticket:0
t2 ticket:5
t2 ticket:4
t2 ticket:3
t2 ticket:2
t2 ticket:1
t2 ticket:0
上述代码和例三功能一样,只是锁定方法不同,这里只是做下演示。
synchronized修饰方法是一种粗颗粒的并发控制,某一时刻只有一个线程执行方法内的内容效率较低下。
synchronized同步块是一种细颗粒的并发控制,可以自行根据需求确定区域较为灵活,可以平衡下效率和安全,同时也能因选择区域不恰当而造成问题。
只要不在synchronized方法(同步块)内的其他部分都不受限制。
普通方法锁定的对象,需要获得对象锁
静态方法锁定的是类,需要获得类锁。
同步块可以确定是锁对象(this )还是锁类(xxx.class),同时也可以自行确定区域。
2.3多线程(java学习笔记)synchronized关键字的更多相关文章
- Java学习笔记18---final关键字修饰变量、方法及类
英语里final这个单词大家都知道是"最终的"意思,其实还有一个意思是"不可更改的".在Java里,final关键字作"不可更改的"来解释更 ...
- 2.1多线程(java学习笔记) java中多线程的实现(附静态代理模式)
一.多线程 首先我们要清楚程序.进程.线程的关系. 首先进程从属于程序,线程从属于进程. 程序指计算机执行操作或任务的指令集合,是一个静态的概念. 但我们实际运行程序时,并发程序因为相互制约,具有“执 ...
- Java学习笔记——static关键字与静态的使用方法
static:可以修饰成员变量和成员方法. 当变量被static修饰后,则其可以直接被类名调用.类名.成员. static特点: 随着类的加载而加载: 优先于对象存在: 被所有的对象共享,节省空间,但 ...
- java学习笔记11-static关键字
如果在类中使用static关键字创建方法,这种方法称为类方法,可以在这个类中直接引用.而不是用static创建的方法.这种方法称为对象方法(实例方法),需要创建对象后才能使用. package les ...
- 0037 Java学习笔记-多线程-同步代码块、同步方法、同步锁
什么是同步 在上一篇0036 Java学习笔记-多线程-创建线程的三种方式示例代码中,实现Runnable创建多条线程,输出中的结果中会有错误,比如一张票卖了两次,有的票没卖的情况,因为线程对象被多条 ...
- Java多线程技术学习笔记(二)
目录: 线程间的通信示例 等待唤醒机制 等待唤醒机制的优化 线程间通信经典问题:多生产者多消费者问题 多生产多消费问题的解决 JDK1.5之后的新加锁方式 多生产多消费问题的新解决办法 sleep和w ...
- Java学习笔记-多线程-创建线程的方式
创建线程 创建线程的方式: 继承java.lang.Thread 实现java.lang.Runnable接口 所有的线程对象都是Thead及其子类的实例 每个线程完成一定的任务,其实就是一段顺序执行 ...
- [java学习笔记]java语言核心----面向对象之this关键字
一.this关键字 体现:当成员变量和函数的局部变量重名时,可以使用this关键字来区别:在构造函数中调用其它构造函数 原理: 代表的是当前对象. this就是所在函数 ...
- java学习笔记14--多线程编程基础1
本文地址:http://www.cnblogs.com/archimedes/p/java-study-note14.html,转载请注明源地址. 多线程编程基础 多进程 一个独立程序的每一次运行称为 ...
- 《Java学习笔记(第8版)》学习指导
<Java学习笔记(第8版)>学习指导 目录 图书简况 学习指导 第一章 Java平台概论 第二章 从JDK到IDE 第三章 基础语法 第四章 认识对象 第五章 对象封装 第六章 继承与多 ...
随机推荐
- PHP vscode+XDebug 远程断点调试服务器上的代码
对于简单的项目或仅仅想知道某一位置的某个变量是什么值,直接使用var_dump配置exit来打印和中断就可以了,方便又快捷, 而对于大型项目的调试,或想了解某个系统的整个运行过程,xdebug可能会是 ...
- Ipython\Jupyter数据分析工具
使用Python进行数据分析优点 1 Python大量的库为数据分析和处理提供了完整的工具集 2 比起R和Matlab等其他主要用于数据分析的编程语言,Python更全能 3 Python库一直在增加 ...
- JavaWeb笔记(七)Filter&Listener
Filter 实现Filter接口 一般用于完成通用的操作,如:登陆验证.统一编码处理.敏感字符过滤等 执行流程 执行过滤器 执行放行后的资源 继续执行过滤器放行代码下的代码 配置 拦截路径配置 注解 ...
- Spring 学习笔记(五)—— Bean之间的关系、作用域、自动装配
继承 Spring提供了配置信息的继承机制,可以通过为<bean>元素指定parent值重用已有的<bean>元素的配置信息. <?xml version="1 ...
- HDU 1939 HE IS OFFSIDE
He is offside! Time Limit: 5000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Total ...
- 【转】通过制作Flappy Bird了解Native 2D中的Sprite,Animation
作者:王选易,出处:http://www.cnblogs.com/neverdie/ 欢迎转载,也请保留这段声明.如果你喜欢这篇文章,请点[推荐].谢谢! 引子 上一次我们讲了MonoBehaviou ...
- Hexo博客收录百度和谷歌-基于Next主题
Hexo博客收录百度和谷歌-基于Next主题(应该是比较全面的一篇教程) 我们的博客做出来当然是希望别人来访问,但是Github和Coding都做了防爬虫处理,这样子我们博客可能就无法被搜索引擎收录, ...
- Citrix NetScaler HA(高可用性)解析
Citrix NetScaler HA(高可用性)解析 来源 https://www.iyunv.com/thread-172259-1-1.html 1.1 NetScaler高可用概述 我 ...
- 冒泡排序 [组合数学+dp]
题面 思路 一眼看过去以为NOI2018的题出出来了= =贼吓人 首先,对于这个难度,我们有一个比较明显的结论: 一个序列的难度,等于这个东西: $hard=max(\sum_{j=i+1}^n[a_ ...
- BZOJ 2458: [BeiJing2011]最小三角形 | 平面分治
题目: 给出若干个点 求三个点构成的周长最小的三角形的周长(我们认为共线的三点也算三角形) 题解: 可以参考平面最近点对的做法 只不过合并的时候改成枚举三个点更新周长最小值,其他的和最近点对大同小异 ...