关键字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. python的sys.args使用

    一.sys 模块 sys是Python的一个「标准库」,也就是官方出的「模块」,是「System」的简写,封装了一些系统的信息和接口. 官方的文档参考:https://docs.python.org/ ...

  2. python基础学习11天,作业题

    1. 文件a.txt内容:每一行内容分别为商品名字,价钱,个数. apple 10 3 tesla 100000 1 mac 3000 2 lenovo 30000 3 chicken 10 3 通过 ...

  3. 剑指Offer-把数组排成最小的数

    题目描述 输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个.例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323. 思路 可以看 ...

  4. linux find 只获取文件名而去除路径

    find /var/process_log/ -name '*.log' -exec basename {} \;

  5. 【blog】推荐一个博客系统后台管理模板 - pinghsu

    pinghsu https://github.com/chakhsu/pinghsu

  6. 推荐前端框架 & 模板

    BootStrap Semantic UI Pure Amazeui(前后端都有,很丰富) amazeui http://tpl.amazeui.org AdminLTE AdminLTE https ...

  7. G - WiFi Password Gym - 101608G (异或思维题+曲尺)

    题目链接:https://cn.vjudge.net/contest/285962#problem/G 题目大意:给你n和m,n代表有n个数,然后让你找出一个最长的区间,使得这个区间内的所有的数的‘’ ...

  8. ASP.NET MVC 入门

    ASP.NET MVC 入门 (Learning ASP.NET MVC) 传统的WebForm发展到如今出现不少的缺陷, 比如为了解决Http的无状态WebForm模式使用了ViewsState来保 ...

  9. P3168 [CQOI2015]任务查询系统

    题目地址:P3168 [CQOI2015]任务查询系统 主席树的模板题 更模板的在这儿:P3834 [模板]可持久化线段树 1(主席树) 形象的说,P3834是"单点修改,区间查询" ...

  10. RocketMQ 简单梳理 及 集群部署笔记【转】

    一.RocketMQ 基础知识介绍Apache RocketMQ是阿里开源的一款高性能.高吞吐量.队列模型的消息中间件的分布式消息中间件. 上图是一个典型的消息中间件收发消息的模型,RocketMQ也 ...