【黑马程序员】java多线程同步不安全问题,锁和死锁等总结_王雨神儿的博客-程序员秘密

------- Windows Phone 7手机开发.Net培训、期待与您交流! -------


java同步关键字:synchronized(同步的)

   1.什么是同步?

  要跨线程维护正确的可见性,只要在几个线程之间共享非 final 变量,就必须使用 synchronized(或 volatile)以确保一个线程可以看见另一个线程做的更改。

为了在线程之间进行可靠的通信,也为了互斥访问,同步是必须的。这归因于java语言规范的内存模型,它规定了:一个线程所做的变化何时以及如何变成对其它线程可见。 

因为多线程将异步行为引进程序,所以在需要同步时,必须有一种方法强制进行。例如:如果2个线程想要通信并且要共享一个复杂的数据结构,如链表,此时需要确保它们互不冲突,也就是必须阻止B线程在A线程读数据的过程中向链表里面写数据(A获得了锁,B必须等A释放了该锁)。

为了达到这个目的,java在一个旧的的进程同步模型——监控器(Monitor)的基础上实现了一个巧妙的方案:监控器是一个控制机制,可以认为是一个很小的、只能容纳一个线程的盒子,一旦一个线程进入监控器,其它的线程必须等待,直到那个线程退出监控为止。通过这种方式,一个监控器可以保证共享资源在同一时刻只可被一个线程使用。这种方式称之为同步。(一旦一个线程进入一个实例的任何同步方法,别的线程将不能进入该同一实例的其它同步方法,但是该实例的非同步方法仍然能够被调用)。

错误的理解:同步嘛,就是几个线程可以同时进行访问。 

同步和多线程关系:没多线程环境就不需要同步;有多线程环境也不一定需要同步。 

锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。 

互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。 

可见性要更加复杂一些,documents它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题

2.为什么我们要使用同步呢?我们来看一下例子代码:


   

package com.itheima;

public class TicketDemo2 {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Ticket t=new Ticket();
		Thread t1=new Thread(t);
		Thread t2=new Thread(t);
		Thread t3=new Thread(t);
		Thread t4=new Thread(t);
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}

}
class Ticket implements Runnable{
	private int tick=100;
	@Override
	public void run() {
		// TODO Auto-generated method stub
		while(tick>0){
			try{
                Thread.sleep(10);
            }catch(InterruptedException e){
                e.printStackTrace();
            }
			System.out.println(tick--);
		}
	}
	
}


根据上述代码,一个买票系统,我们想要递减的方式把票卖完,可是打印结果会出现0 、 -1甚至-2这样的结果,这是不正常的。为什么会出现这种情况呢?

因为java多线程在访问同一个资源的时候会出现安全问题。

上述代码中循环条件为tick>0才继续卖票继续执行循环。但是多线程的话,可能比如当tick等于1的时候,这个时候4个线程都满足条件,都进入到了while循环内了,这个时候就出现了0 、-1、-2这样的结果。

        那么怎么解决这个问题呢?就是使用同步!

解决方法:多个线程操作共享数据的时候,操作共享数据的代码块每次只能由一个线程执行,只有当前线程执行完后,其他线程才可以进来执行,并且每次只能由一个线程来执行。

同步前提是多线程。

java同步关键字:synchronized(对象){同步代码块}

使用方法例子:

package com.itheima;

public class TicketDemo2 {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Ticket t=new Ticket();
		Thread t1=new Thread(t);
		Thread t2=new Thread(t);
		Thread t3=new Thread(t);
		Thread t4=new Thread(t);
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}

}
class Ticket implements Runnable{
	private int tick=100;
	@Override
	public void run() {
		// TODO Auto-generated method stub
		
		while(true){
			try{
                Thread.sleep(10);
            }catch(InterruptedException e){
                e.printStackTrace();
            }
            synchronized(this){
            	if(tick>0){

                	System.out.println(tick--);
            	}
            }
			
		}
	}
	
}

Java同步机制有4种实现方式:(部分引用网上资源) 

①    ThreadLocal ② synchronized( ) ③ wait() 与 notify() ④ volatile 

目的:都是为了解决多线程中的对同一变量的访问冲突 
ThreadLocal 
    ThreadLocal 保证不同线程拥有不同实例,相同线程一定拥有相同的实例,即为每一个使用该变量的线程提供一个该变量值的副本,每一个线程都可以独立改变自己的副本,而不是与其它线程的副本冲突。

优势:提供了线程安全的共享对象 

与其它同步机制的区别:同步机制是为了同步多个线程对相同资源的并发访问,是为了多个线程之间进行通信;而 ThreadLocal 是隔离多个线程的数据共享,从根本上就不在多个线程之间共享资源,这样当然不需要多个线程进行同步了。

volatile 
     volatile 修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。 
    优势:这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。 
    缘由:Java 语言规范中指出,为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才与共享成员变量的原始值对比。这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化。而 volatile 关键字就是提示 VM :对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。
     使用技巧:在两个或者更多的线程访问的成员变量上使用 volatile 。当要访问的变量已在 synchronized 代码块中,或者为常量时,不必使用。
        线程为了提高效率,将某成员变量(如A)拷贝了一份(如B),线程中对A的访问其实访问的是B。只在某些动作时才进行A和B的同步,因此存在A和B不一致的情况。volatile就是用来避免这种情况的。 volatile告诉jvm,它所修饰的变量不保留拷贝,直接访问主内存中的(读操作多时使用较好;线程间需要通信,本条做不到)

   Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。这就是说线程能够自动发现 volatile 变量的最新值。Volatile 变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。

            您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件: 

对变量的写操作不依赖于当前值;该变量没有包含在具有其他变量的不变式中。 



sleep() vs wait() 
sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,把执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。
wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。

(如果变量被声明为volatile,在每次访问时都会和主存一致;如果变量在同步方法或者同步块中被访问,当在方法或者块的入口处获得锁以及方法或者块退出时释放锁时变量被同步。)



2.死锁

死锁就是,锁不是都有对象吗?方法1中同步代码块A锁里嵌套一个B锁,方法2中同步代码块中有B锁里嵌套一个A锁,这样的一个嵌套,当多线程执行后。线程1执行到方法1A锁的时候,线程2也同时执行了方法2中的B锁。这时候方法1里面的B锁被方法2锁定,方法2中要执行A锁的时候被方法1锁定,相持不下,程序被锁定了,这个时候被称为死锁。

       

package com.itheima;

public class DeadLock {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Ticket2 t1=new Ticket2(true);
		Ticket2 t2=new Ticket2(false);
		Thread th1=new Thread(t1);
		Thread th2=new Thread(t2);
		th1.start();
		th2.start();
	}

}
class LockTest{
	static Object o1=new Object();
	static Object o2=new Object();
}
class Ticket2 implements Runnable{
	boolean b;
	Ticket2(){}
	Ticket2(boolean b){
		this.b=b;
	}
	
	private int tick=100;
	@Override
	public void run() {
		// TODO Auto-generated method stub
		
		if(b){
			while(true){
	            synchronized(LockTest.o1){
	            	synchronized(LockTest.o2){
	            		System.out.println("LockTest.01");
	            		if(tick>0){
	
	                		System.out.println(tick--);
	            		}
	            	}
	            }
			}
			
		}else{
			while(true){
				synchronized(LockTest.o2){
					System.out.println("LockTest.02");
	            	synchronized(LockTest.o1){
	            		if(tick>0){
	
	                		System.out.println(tick--);
	            		}
	            	}
	            }
			}
		}
	}
	
}

为什么学习死锁呢?因为只有你了解了死锁,在你写程序的时候才可以避免死锁了解多线程。


------- Windows Phone 7手机开发.Net培训、期待与您交流! -------

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Ly_douya/article/details/16368319

智能推荐

2021年N1叉车司机考试总结及N1叉车司机证考试_雨天里的他的博客-程序员秘密

题库来源:安全生产模拟考试一点通公众号小程序2021年N1叉车司机考试总结及N1叉车司机证考试,包含N1叉车司机考试总结答案和解析及N1叉车司机证考试练习。由安全生产模拟考试一点通公众号结合国家N1叉车司机考试最新大纲及N1叉车司机考试真题汇总,有助于N1叉车司机作业考试题库考前练习。1、【判断题】《场(厂)内专用机动车辆安全技术监察规程》中规定,液力传动的叉车,不一定具有微动功能。(×)2、【判断题】《场(厂)内专用机动车辆安全技术监察规程》中规定,机械传动的叉车,换挡应有同步器。...

windows下配置nginx反向代理_cookies_token的博客-程序员秘密

Nginx官网http://nginx.org/en/download.html下载稳定版本(Stable version),这里下载的是:windows 1.18.0部署过程1、下载完成后,解压缩就可以,不要直接双击nginx.exe,直接运行nginx.exe会导致修改配置无效启动等不生效2、修改conf下的配置文件nginx.conf,有些项目需要拿到访问者的IP地址等,最好也在proxy_pass后面紧跟配置参数↓↓↓↓↓↓↓↓↓↓↓↓↓#这里是如果没设置这个头,...

[Bootstrap]网格系统_kzl_knight的博客-程序员秘密

<768>=768>=992>=1200前缀.col-xs.col-sm.col-md.col-lg偏移列col-sm-offset-【数字】列排序col-sm-push-【数字】 相对定位向右偏移col-sm-pull-【数字】 相对定位向做偏移...

配置的ANT_HOME环境变量并把%ANT_HOME%\bin 加入到系统PATH环境变量后无法执行ant命令_ant home_hello5orld的博客-程序员秘密

若要在windows的命令行使用ant的话,需要为ant配置环境变量,但是如果不细心的话,会出现如下的问题:配置的ANT_HOME环境变量并把%ANT_HOME%\bin 加入到系统PATH环境变量后无法执行ant命令造成这个错误的原因是:当把%ANT_HOME%\bin这个变量加入到系统的PATH环境变量时,加入到了其他变量的左边而不是右边,例如        %ANT_

DVWA-File Inclusion实战篇--文件包含漏洞_喜洋洋&1的博客-程序员秘密

DVWA实战篇--文件包含漏洞File Inclusion漏洞介绍为了使代码更加灵活,通常会将被包含的文件设置为变量,用来进行动态调用,但正是由于这种灵活性,从而导致客户端可以调用一个恶意文件,造成文件包含漏洞。对一个简单的 PHP 小程序来说,在不同的 PHP 脚本之间剪切或复制某一函数不是大问题, 但是当进入项目开发时,函数的数量将会变得相当庞大,并且函数具有较强的复杂性,这时你就会把它们保存到一个便于随时调用的函数库中,以便于该函数在整个项目中可以随时被调用。通常情况下这个函数库是一个文.

Unity3D 自定义光照模型实现_FreedomRoad~的博客-程序员秘密

// Use BlinnPhongShader "U1/Obj/U1ObjNoAmbient" {Properties{_Color("Diffuse Material Color", Color) = (1,1,1,1)_SpecColor("Specular Material Color", Color) = (1,1,1,1)_MainTex("Base (RGB

随便推点

错误 1 error C2065: "endl": 未声明的标识符_daa20的博客-程序员秘密

//f0513//函数重载:C++编译器根据函数参数的类型,数量和排列顺序的差异,来区分同名函数,其技术称为重载技术//相应的同名函数称为重载函数#include //using namespace std;int abs(int a){ return (a>0)?a:-a;}double abs(double a){ return (a>0)?a:-a;}int ma

SpringBoot---SpringBoot-JPA_hay_lee的博客-程序员秘密

JPA 应该都熟悉了,我就不多说了什么是JPA了。目前JPA主要实现由hibernate和openJPA等。Spring Data JPA 是Spring Data 的一个子项目,它通过提供基于JPA的Repository极大了减少了操作JPA的代码。笔者觉得这个由SpringBoot 提供的JPARepository真的是非常爽。基本上大部分的业务都可以满足了。在Spring环境中需要配置大量了...

深入浅出DPDK之内存特点和IOVA_攻城狮百里的博客-程序员秘密

大页DPDK通常是使用大页(hugepage)内存的,无论是2M的大页还是1G的大页,本质上都是为了减少TLB miss,通过更大的page size来提升TLB的命中率,而TLB就是用来缓存页表的高速缓存。DMA我们知道计算机的设备,如网卡硬件是不能处理用户空间的虚拟地址(只有CPU通过页表转换MMU才能识别虚拟地址),因为它不能感知任何用户态的进程和其所分配到的用户空间虚拟地址。相反,它只能访问真实的物理地址上的内存。出于对效率的考量,现代硬件几乎总是使用直接内存存取(DMA)事务。通常,为了执

javascript自动触发点击事件_城永的博客-程序员秘密

<!DOCTYPE html><html> <head> <meta charset="utf-8"> <title></title> </head> <body> <button id="d1">你好</button> <script type="text/javascript"> setTimeout(()=>{ // 兼容.

C语言编写一个代码,实现多个字符从两端移动向中间汇聚的功能_c语言如何把两个功能汇聚在一个程序中_Hansionz的博客-程序员秘密

【功能演示】:逐行输出为以下效果【参考代码】:#include<stdio.h>#include<string.h>#include<windows.h>int main(){ char arr1[] = "Good good study day day up!"; char arr2[] = "##########################...

uniapp font-family设置无效问题解决_爱踢球的小罗的博客-程序员秘密

uniapp font-family设置无效问题解决​ 只需要在使用font-family的位置,添加如下代码​ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;...

推荐文章

热门文章

相关标签