高并发编程基础Synchronized与Volatile
关键字Synchronized:
当使用Synchrnized (o) ,锁定 o 的时候,锁定的是 o 指向的堆内存中 new 出来的对象,而非 o 引用,当锁定 o 以后,一旦 o 指向了其他对象,这个时候锁定的对象也会发生改变。在工作开发中经常 new 出一个对象当锁太麻烦了,常用的方法是锁定执行方法的对象,即 Synchronized (this)。任何线程要执行同步的代码,必须先获得 this 的锁。锁定 this 对象还有一种写法就是写在方法申明上 public synchronized void method(){} .需要注意的是对于静态(static)方法中不可以使用Synchronized (this),因为静态的属性或方法不需要 new 出来对象来访问的,也就是没有 this 引用的存在 。Synchronized所同步的代码块越少越好,细粒度的锁能提高效率 下面看一些例子要进一步认识Synchronized。
public class Test5 implements Runnable{
private int count =10;
@Override
public /*synchronized*/ void run() {// synchronized的代码块是原子操作,不可分,只要当前线程操作完了,其他线程才能访问
count --;
// 线程重入 当线程1执行到这里,线程2,3.。也执行到这里,所有控制台有可能输出不一致问题。控制台打印如下。
// Thread0count:8
// Thread4count:5
// Thread2count:6
// Thread1count:8
// Thread3count:7
System.err.println(Thread.currentThread().getName()+ "count:"+count);
}
public static void main(String[] args) {
Test5 t =new Test5();
for(int i=0;i<5;i++) {
new Thread(t,"Thread"+i ).start();
}
}
}
要解决以上线程重入,只需要在方法上添加关键字Synchronized 即可。 因为Synchronized的代码块是原子操作,不可分,不可以被打断。就能避免线程重入。
public class Test6{
public synchronized void m1() {
System.err.println(Thread.currentThread().getName()+ "m1 start:");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.err.println(Thread.currentThread().getName()+ "m1 end:");
}
public void m2() {
System.err.println(Thread.currentThread().getName()+ "m2 start:");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.err.println(Thread.currentThread().getName()+ "m2 end:");
}
public static void main(String[] args) {
Test6 t =new Test6();
//再调用m1的过程之中能否访问m2 当然可以
new Thread(t::m1,"t1").start();
new Thread(t::m2,"t2").start();
}
}
上面这个小例子要说明的是,当同步方法被调用的过程中能否调用非同步方法,通过执行以上代码可以发现,是可以的。
public class Account{
String name;
double balance;
public synchronized void set(String name ,double balance) {
this.name=name;
try { // 放大线程执行的时间差,表明有可能在执行过程中有其他线程来通过getBalance()方法获取balance;
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.balance=balance;
}
public double getBalance(String name) {
return this.balance;
}
public static void main(String[] args) {
Account a =new Account();
new Thread(()->a.set("zhangsan",100.0)).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(a.getBalance("zhangsan"));
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(a.getBalance("zhangsan"));
}
}
上述代码对业务写方法加锁,读方法不加锁,容易造成脏读,要解决以上问题,只要在getBalance()上加Synchronized就可以。
public class Test7{
public synchronized void m1() {
System.err.println(Thread.currentThread().getName()+ "m1 start:");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
m2();
}
public synchronized void m2() {
System.err.println(Thread.currentThread().getName()+ "m2 start:");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.err.println(Thread.currentThread().getName()+ "m2 end:");
}
public static void main(String[] args) {
Test7 t =new Test7();
//再调用m1的过程之中能否访问m2 当然可以
new Thread(t::m1,"t1").start();
}
}
上诉代码阐述了一个问题,一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候还会得到该对象的锁,也就是说synchronized的锁是可以重入的。由于这里锁定是同一个对象this.所以不会产生死锁,产生死锁的情况有很多,其中最简单的一种情况入下:
public static void main(String[] args) {
Object a =new Object();
Object b =new Object();
new Thread(()->{
synchronized(a) {
System.out.println("锁定a");
try {
TimeUnit.SECONDS.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized(b) {
System.out.println("锁定b");
}
}
},"t1").start();
new Thread(()->{
synchronized(b) {
System.out.println("锁定b");
synchronized(a) {
System.out.println("锁定a");
}
}
},"t2").start();
}
重入锁还有另外一种情形,即子类的同步方法调用父类的同步方法,其本职也是锁定this对象。代码如下:
public class Test8{
public synchronized void m1() {
System.err.println( "m1 start:");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.err.println( "m1 end:");
}
public static void main(String[] args) {
new TT().m1();;
}
}
class TT extends Test8{
@Override
public synchronized void m1() {
System.err.println( " child m1 start:");
super.m1();
System.err.println( " child m1 end:");
}
}
下面来看一下另外一个问题,当线程在执行过程中如果有异常抛出的话会产生什么样的后果呢?
public class Test9{
int count =0;
public synchronized void m1() {
System.err.println(Thread.currentThread().getName()+ " start:");
while(true) {
count ++;
System.err.println(Thread.currentThread().getName()+ " count :"+count);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(count ==5) {
int i=1/0;
}
}
}
public static void main(String[] args) {
Test9 t =new Test9();
//再调用m1的过程之中能否访问m2 当然可以
new Thread(t::m1,"t1").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
new Thread(t::m1,"t2").start();
}
}
上述代码执行过程中抛出了 ArithmeticException ,抛出异常后该线程立马会释放锁。可以看到运行该程序后,在异常抛出后,t2线程会执行,即证明了t1释放锁。然后t2会拿着t1执行了一半的数据再去处理自己的业务,在程序中这样会发生很严重的数据问题。所以在同步方法内如果会抛出异常,一定要堆异常进行适当的处理,避免类似问题的出现。
public class Test12 {
Object o = new Object();
void m() {//
synchronized (o) {//锁的是堆内存里new出来的对象,一旦o指向了另外的对象,那么原来的锁将被释放
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.err.println(Thread.currentThread().getName() );
}
}
}
public static void main(String[] args) {
Test12 t = new Test12();
new Thread(t::m,"t1").start();//启动第一个线程
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//启动第二个线程
Thread t2 = new Thread(t::m,"t2");//锁对象发生改变 t2才能执行
t.o =new Object();
t2.start();
}
}
上述小程序描述了synchronized 锁,锁的是堆内存里new出来的对象,一旦o指向了另外的对象,那么原来的锁将被释放,通过上述小程序运行发现当执行完t.o =new Object(); t2线程会随即运行,不然一定要等t1线程释放锁t2才得以运行。
注释掉 t.o =new Object(); 会发现t2是无法运行的。
关键字 Volatile :
先来看一下一段小程序:
public class Test10{
/*volatile*/ boolean running =true; //对比有无 volatile的情况下,整个程序的执行结果
public synchronized void m1() {
System.err.println(Thread.currentThread().getName()+ " start:");
while(running) {
}
System.err.println(Thread.currentThread().getName()+ " end:");
}
public static void main(String[] args) {
Test10 t =new Test10();
new Thread(t::m1,"t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
t.running=false;
}
}
执行小程序,我们会发现在没有 volatile 的情况下,程序一直会处于运行的情况,也就是其中变量running一直是 true 的状态,从而导致方法 m1 一直处于死循环的状态,当加上了关键字 volatile 后,程序会结束,这是为什么呢? 其实这其中就涉及了线程中 running 这个变量可见性的问题,也就是线程之间的通讯问题,这里涉及到 JAVA 对于线程处理的内存模型(Java Memory Model),在Java Memory Model里面有一个内存叫主内存,我们所说的栈内存,堆内存,都可以认为是主内存,每一个线程在执行的过程中,都会有自己的一块内存,而这块内存不是真正的内存,实际上是CPU上的一块缓冲区,其实就是存放线程自己的变量的一块内存区,它的作用是在线程运行时,将主内存中的内容读过来在缓冲区内做修改,执行完修改动作了再将结果写回到主内存。但是在处理的过程中线程不会再去到主内存中读取该内容,上述代码中由于while死循环使得CPU非常的繁忙,他就不再去主内存中读取running的值,而是直接再自己的缓冲区中读取该变量的值,但是在其他情况下,当CPU并不是那么的繁忙的时候,还是会去读一下的。主线程把 running 改成了 false ,但是t1 线程没有去主内存中重新获取running的值,由于缓冲区中running的值是true,所以导致线程一直处于死循环。加了 volatile 之后,在 running 的值发生了改变,会通知其他线程 ,你们的缓冲区中 running 的值过期了,这个时候,其他线程才会去主内存中重新获取 running 的值,从而才能使线程结束。
如果不使用 volatile 的话,可以使用 synchornized ,但是性能方面会大幅度降低, volatile 的性能比 synchronized的性能要好得多。volatile 可以说使无锁同步,使得两个线程之前的变量的可见性。
volatile不能保证多个线程共同修改running的值带来的不一致问题,也就是说 volatile 不能代替 synchronized ,synchronized即保证了原子性,也保证了可见性,而volatile仅仅保证了可见性,来看一下下一个小程序:
public class Test11{
volatile int count =0;//只保证可见性
void m() {
for(int i=0;i<10000;i++) {
count ++;
}
}
public static void main(String[] args) {
Test11 t =new Test11();
List<Thread> threads =new ArrayList<Thread>();
for(int i=0;i<10;i++) {
threads.add(new Thread(t::m,"thread"+i));
}
threads.forEach((o)->o.start());
threads.forEach((o)->{
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t.count);
}
}
理论上上述小程序输出的结果会是100000,但是结果并不是如此,为什么会这样呢? 因为volatile 仅仅保证可见性,当两个线程同时读取到count为 10的时候,线程1 对count 执行完++ 以后,将11写回,此刻线程2也将自己的++以后的结果写回,这个时候就会出现这种问题,线程2覆盖了线程1 的结果。实际上就加了一遍,如果有多个线程,可能发生多次覆盖。要解决这个问题,可以使用 synchronized ,在方法 m 前 加上synchronized,去掉 count 的volatile即可。如果程序中仅仅涉及数字的简单加减。可以使用JAVA提供的原子类 AtomicXXX来进行操作。因为AtomicXXX这些类所提供的方法都是原子性的,但是AtomicXXX类两个方法之间是不具备原子性的,比如AtomicInteger 的++ 可以使用incrementAndGet()方法等等。修改以上代码如下:
public class Test11{
// /*volatile*/ int count =0;//只保证可见性
AtomicInteger count =new AtomicInteger(0);
// AtomicBoolean ,AtomicLong
/*synchronized*/ void m() {
for(int i=0;i<10000;i++) {
//count ++;
//if count.get() <1000 再这句中间和下面一行代码之间是不具备原子性的
count.incrementAndGet();// 具备原子性 代替count++
}
}
public static void main(String[] args) {
Test11 t =new Test11();
List<Thread> threads =new ArrayList<Thread>();
for(int i=0;i<10;i++) {
threads.add(new Thread(t::m,"thread"+i));
}
threads.forEach((o)->o.start());
threads.forEach((o)->{
try {
o.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
System.out.println(t.count);
}
}
高并发编程基础Synchronized与Volatile的更多相关文章
- Java高并发编程基础三大利器之CountDownLatch
引言 上一篇文章我们介绍了AQS的信号量Semaphore<Java高并发编程基础三大利器之Semaphore>,接下来应该轮到CountDownLatch了. 什么是CountDownL ...
- java并发编程(2) --Synchronized与Volatile区别
Synchronized 在多线程并发中synchronized一直是元老级别的角色.利用synchronized来实现同步具体有一下三种表现形式: 对于普通的同步方法,锁是当前实例对象. 对于静态同 ...
- 高并发编程基础(java.util.concurrent包常见类基础)
JDK5中添加了新的java.util.concurrent包,相对同步容器而言,并发容器通过一些机制改进了并发性能.因为同步容器将所有对容器状态的访问都串行化了,这样保证了线程的安全性,所以这种方法 ...
- 《JAVA高并发编程详解》-volatile和synchronized
- java高并发编程基础之AQS
引言 曾经有一道比较比较经典的面试题"你能够说说java的并发包下面有哪些常见的类?"大多数人应该都可以说出 CountDownLatch.CyclicBarrier.Sempah ...
- Java 面试知识点解析(二)——高并发编程篇
前言: 在遨游了一番 Java Web 的世界之后,发现了自己的一些缺失,所以就着一篇深度好文:知名互联网公司校招 Java 开发岗面试知识点解析 ,来好好的对 Java 知识点进行复习和学习一番,大 ...
- Java并发编程基础三板斧之Semaphore
引言 最近可以进行个税申报了,还没有申报的同学可以赶紧去试试哦.不过我反正是从上午到下午一直都没有成功的进行申报,一进行申报 就返回"当前访问人数过多,请稍后再试".为什么有些人就 ...
- [ 高并发]Java高并发编程系列第二篇--线程同步
高并发,听起来高大上的一个词汇,在身处于互联网潮的社会大趋势下,高并发赋予了更多的传奇色彩.首先,我们可以看到很多招聘中,会提到有高并发项目者优先.高并发,意味着,你的前雇主,有很大的业务层面的需求, ...
- Java 多线程高并发编程 笔记(一)
本篇文章主要是总结Java多线程/高并发编程的知识点,由浅入深,仅作自己的学习笔记,部分侵删. 一 . 基础知识点 1. 进程于线程的概念 2.线程创建的两种方式 注:public void run( ...
随机推荐
- 安装LDAP用户认证
LDAP伺服器设定 1.安装 openldap-servers yum -y install openldap openldap-devel openldap-servers 2.建立 LDAP 密码 ...
- day 11 - 1 装饰器
装饰器 装饰器形成的过程:最简单的装饰器——有返回值的——有一个参数——万能参数装饰器的作用:不想修改函数的调用方式 但是还想在原来的函数前后添加功能原则:开放封闭原则语法糖:@装饰器函数名装饰器的固 ...
- Java的两大数据类型
Java的两大数据类型 基本数据类型 byte,short,int,long,float,double,boolean,char byte 类别 内容 类型 byte 简介 byte 数据类型是8位. ...
- G - Galactic Collegiate Programming Contest Kattis - gcpc (set使用)
题目链接: G - Galactic Collegiate Programming Contest Kattis - gcpc 题目大意:当前有n个人,一共有m次提交记录,每一次的提交包括两个数,st ...
- TPU使用说明
1 TPU分类和收费标准 1.1 分类和计费说明 地区 抢占式TPU Cloud TPU 美国 $1.35/hour $4.5/hour 欧洲 $1.485/hour $4.95/hour 亚太区地区 ...
- webrtc学习笔记
获取笔记本摄像头视频流 <html> <meta http-equiv="Content-Type" content="text/html; chars ...
- SpringSecurityOAuth使用JWT Token
⒈JWT? JWT(Json Web Token),是Json的一个开放的Token标准. 1,自包含,SpringSecurityOAuth的默认Token是UUID的一个随机的无意义的字符串,并不 ...
- 数字图像处理的Matlab实现(1)—绪论
第1章 绪论 1.1 什么是数字图像处理 一幅图像可以定义为一个二维函数\(f(x,y)\),这里的\(x\)和\(y\)是空间坐标,而在任意坐标\((x,y)\)处的幅度\(f\)被称为这一坐标位置 ...
- 【转】python模块分析之logging日志(四)
[转]python模块分析之logging日志(四) python的logging模块是用来写日志的,是python的标准模块. 系列文章 python模块分析之random(一) python模块分 ...
- LwIP Application Developers Manual10---LwIP IPv4/IPv6 stacks
1.前言 lwIP正在加入IPv6,一个实验性的版本可以通过git下载,该版本实现了一个IPv4/IPv6的双协议栈.通过在lwipopts.h定义LWIP_IPV6可以使能IPv6 2.已实现的IP ...