关键字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的更多相关文章

  1. Java高并发编程基础三大利器之CountDownLatch

    引言 上一篇文章我们介绍了AQS的信号量Semaphore<Java高并发编程基础三大利器之Semaphore>,接下来应该轮到CountDownLatch了. 什么是CountDownL ...

  2. java并发编程(2) --Synchronized与Volatile区别

    Synchronized 在多线程并发中synchronized一直是元老级别的角色.利用synchronized来实现同步具体有一下三种表现形式: 对于普通的同步方法,锁是当前实例对象. 对于静态同 ...

  3. 高并发编程基础(java.util.concurrent包常见类基础)

    JDK5中添加了新的java.util.concurrent包,相对同步容器而言,并发容器通过一些机制改进了并发性能.因为同步容器将所有对容器状态的访问都串行化了,这样保证了线程的安全性,所以这种方法 ...

  4. 《JAVA高并发编程详解》-volatile和synchronized

  5. java高并发编程基础之AQS

    引言 曾经有一道比较比较经典的面试题"你能够说说java的并发包下面有哪些常见的类?"大多数人应该都可以说出 CountDownLatch.CyclicBarrier.Sempah ...

  6. Java 面试知识点解析(二)——高并发编程篇

    前言: 在遨游了一番 Java Web 的世界之后,发现了自己的一些缺失,所以就着一篇深度好文:知名互联网公司校招 Java 开发岗面试知识点解析 ,来好好的对 Java 知识点进行复习和学习一番,大 ...

  7. Java并发编程基础三板斧之Semaphore

    引言 最近可以进行个税申报了,还没有申报的同学可以赶紧去试试哦.不过我反正是从上午到下午一直都没有成功的进行申报,一进行申报 就返回"当前访问人数过多,请稍后再试".为什么有些人就 ...

  8. [ 高并发]Java高并发编程系列第二篇--线程同步

    高并发,听起来高大上的一个词汇,在身处于互联网潮的社会大趋势下,高并发赋予了更多的传奇色彩.首先,我们可以看到很多招聘中,会提到有高并发项目者优先.高并发,意味着,你的前雇主,有很大的业务层面的需求, ...

  9. Java 多线程高并发编程 笔记(一)

    本篇文章主要是总结Java多线程/高并发编程的知识点,由浅入深,仅作自己的学习笔记,部分侵删. 一 . 基础知识点 1. 进程于线程的概念 2.线程创建的两种方式 注:public void run( ...

随机推荐

  1. Spark思维导图之Spark Streaming

  2. rem+js响应式布局的设置

    直接调用代码即可,不过不同屏幕宽度要求会不同,相应修改一下就ok了 // rem响应式布局 (function(){ var html=document.querySelector('html') h ...

  3. 列举两种不同类型的Java标识注释,并解释它们之间的区别。

    列举两种不同类型的Java标识注释,并解释它们之间的区别.

  4. Required String parameter 'images' is not present

    后台控制层控制为非必填即可: @RequestMapping("/addDo") @SJson @SLog(description = "Car_main") ...

  5. Django 基于类的通用视图

    在早期,我们认识到在视图开发过程中有共同的用法和模式.这时我们引入基于函数的通用视图来抽象这些模式以简化常见情形的视图开发. 基于函数视图的用法有以下三种: def index(request): r ...

  6. pythonのsqlalchemy外键关联查询

    #!/usr/bin/env python import sqlalchemy from sqlalchemy import create_engine from sqlalchemy.ext.dec ...

  7. 关于Mac或Linux下GO的Permission denied提示错误

    有时候当你下载第三方库的时候,编译时会提示Permission denied 权限不足, 出现这种错误因为权限不够.其中一种办法是需要把你项目目录和go的pck.bin权限放开. chmod -R 7 ...

  8. css干货部分

    1.css的引入方式(三种): a.行内样式<div> <p style="color: green">我是一个段落</p> </div& ...

  9. Pytorch中RoI pooling layer的几种实现

    Faster-RCNN论文中在RoI-Head网络中,将128个RoI区域对应的feature map进行截取,而后利用RoI pooling层输出7*7大小的feature map.在pytorch ...

  10. Linux RTC驱动模型分析之rtc-sysfs.c【转】

    转自:https://blog.csdn.net/longwang155069/article/details/52353408 版权声明:本文为博主原创文章,未经博主允许不得转载. https:// ...