Java 多线程(二) synchronized关键字

1.概要
  • 线程安全:多个线程同时访问公共对象或者同一个对象时,采用了加锁的机制,对公共数据进行保护,直到线程对该数据使用完。
  • 非线程安全:多个线程同时访问公共对象或者同一个对象时,发生数据不一致或者数据污染
  • 脏读:读到的数据其实是被更改过的,数据不一致或者数据污染。
2.Synchronized方法与锁对象
  • synchronized

1.监视器:每个对象都有一个监视器(monitor),它允许每个线程同时互斥和协作,就像每个对象都会有一块受监控的区域

 (数据结构),当线程执行需要取到监控区域的数据时,首先验证是否有线程拥有监视器,已有线程拥有监视器则进入监视

 器的monior entry list进行等待Thread.state:BLOCKED,直到释放退出监控区域且释放锁。以这种FIFO的方式等待。

  • 对象锁:每个对象在堆内存中的头部都会维持一块锁区域,任何线程要同步执行对象数据都会放入监视器且都必须获锁。

 一个线程可以允许多次对同一对象上锁.对于每一个对象来说,java虚拟机维护一个计数器,记录对象被加了多少次锁,没被锁的

 对象的计数器是0,线程每加锁一次,计数器就加1,每释放一次,计数器就减1.当计数器跳到0的时候,锁就被完全释放了

synchronized工作机制是这样的:Java中每个对象都有一把锁与之相关联,

锁控制着对象的synchronized代码。一个要执行对象的synchronized代码

的线程必须先获得那个对象的锁。

  • synchronized 同步方法

 一个对象只有一把对象锁

 synchronized获取的是对象锁,保证线程顺序进入对象方法。

public class test {
	public void testSynchronized(){
		try {
			System.out.println("start:"+Thread.currentThread().getName());
			Thread.sleep(5000);
			
			System.out.println("end");
		} catch (Exception e) {
			// TODO: handle exception
		}
	}
	public static void main(String[] args) {
		test t = new test();
		ThreadA threadA = new ThreadA(t);
		threadA.setName("A");
		threadA.start();
		
		ThreadB threadB = new ThreadB(t);
		threadB.setName("B");
		threadB.start();
	}
}
 执行结果:可以看出线程是并行运行的。
start:A
start:B
end
end

 在方法加入同步synchronized关键字,执行结果:线程同步顺序执行

start:A
end
start:B
end
  锁重入:关键字synchronized拥有锁重入的功能,当一个线程得到对象锁后在当前线程能够再次获得此对象锁,这说明一个

线程在得到对象锁后可以无限制的获得对象锁。

public class test {
	
	public void methodA(){
		System.out.println("非synchronized方法");
	}
	
	public synchronized void testSynchronized(){
		try {
			System.out.println("start:"+Thread.currentThread().getName());
			methodB();
			
		} catch (Exception e) {
			// TODO: handle exception
		}
	}
	
	public synchronized void methodB() throws InterruptedException{
		Thread.sleep(5000);
		System.out.println("end");
	}
	public static void main(String[] args) {
		test t = new test();
		ThreadA threadA = new ThreadA(t);
		threadA.setName("A");
		threadA.start();
		
		ThreadB threadB = new ThreadB(t);
		threadB.setName("B");
		threadB.start();
	}
}
 执行结果:自己可以再次获取自己的内部对象锁,当线程获取到对象锁后在其内部还可以获得对象锁,如果锁不可重入的话

就很容易造成死锁,因为外部对象锁还未释放,导致在内部永远获取不到对象锁,线程永远处于等待。

start:A
end
start:B
end
 出现异常,锁会自动释放:当一个线程执行代码是出现异常时,会自动释放所持有的对象锁。


 同步不具有继承性:当父类方法进行同步,子类重写该方法,子类方法不具有同步性,需要添加synchronized关键字。

  • synchronized同步代码块

 用synchronized同步方法是有弊端的,当方法某一条语句执行时间过长,就会导致其他线程需要等待较长时间。所以同步 代码块可以相对提高效率。

  • 同步代码块的使用

synchronized需要依赖于对象锁,同步代码块是需要一个锁对象,可以是当前对象(this),一般系统并发量很高不采用当

前对象,而采用任意其他一个对象,不然造成大量线程等待在该对象。

public class test {
	public void methodA(){
		System.out.println("非synchronized方法");
	}
	
	public  void testSynchronized(){
		try {
			System.out.println("start:"+Thread.currentThread().getName());
			synchronized (this) {
				methodB();
			}
			
		} catch (Exception e) {
			// TODO: handle exception
			e.printStackTrace();
		}
	}
	public synchronized void methodB() throws Exception{
		Thread.sleep(5000);
		System.out.println("end");
	}
	public static void main(String[] args) {
		test t = new test();
		ThreadA threadA = new ThreadA(t);
		threadA.setName("A");
		threadA.start();
		
		ThreadB threadB = new ThreadB(t);
		threadB.setName("B");
		threadB.start();
	}
}
 执行结果:线程A、B并发进入方法,但有一个线程等待在同步代码块前。
 
start:A
start:B
end
end
 任意对象锁:在同步代码块不取this(当前对象)锁,而采用任意对象锁,这样的好处是当有很多synchronized同步方法时

,如果用this对象锁,会造成阻塞,而采用任意锁,其他同步块则不会造成阻塞。

public class test {
	private Object obj = new Object();
	public void methodA(){
		System.out.println("非synchronized方法");
	}
	
	public  void testSynchronized(){
		try {
			System.out.println("start:"+Thread.currentThread().getName());
			synchronized (this) {
				methodB();
			}
			
		} catch (Exception e) {
			// TODO: handle exception
			e.printStackTrace();
		}
	}
	
	public synchronized void methodB() throws Exception{
		Thread.sleep(5000);
		System.out.println("end");
	}
	
	public void methodC(){
		synchronized (obj) {
			try {
				System.out.println("start:"+Thread.currentThread().getName());
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
	
	public static void main(String[] args) {
		test t = new test();
		ThreadA threadA = new ThreadA(t);
		threadA.setName("A");
		threadA.start();
		
		ThreadB threadB = new ThreadB(t);
		threadB.setName("B");
		threadB.start();//执行methodC
	}
}

 执行结果:线程A使用当前this对象锁同步代码块,线程B能够同步执行OBJ对象锁进行同步代码块

start:A
start:B
end
 同步代码块解锁无限等待问题:同步方法获得的是当前对象的对象锁,一单其中一个同步方法陷入死循环,该对象的其他同

步方法都无限等待,所以同步需要同步的部分代码块且使用任意对象锁。

  • synchronized同步静态方法

 关键字synchronized还可以修饰static方法,如果这样写,那是对当前的.java文件的class对象进行加锁。

  • 与实例方法取得不同的对象锁

 Java程序在运行时,Java运行时系统一直对所有的对象进行所谓的运行时类型标识,即所谓的RTTI。这项信息纪录了

每个对象所属的类。虚拟机通常使用运行时类型信息选准正确方法去执行,用来保存这些类型信息的类是Class类。

Class类封装一个对象和接口运行时的状态,当装载类时,Class类型的对象自动创建。

public class test {
	private Object obj = new Object();
	public synchronized void methodA(){
		System.out.println("start not static :"+Thread.currentThread().getName());
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println("static end");
	}
	
	public synchronized static void testSynchronized(){
		try {
			System.out.println("start:"+Thread.currentThread().getName());
			methodB();
			
		} catch (Exception e) {
			// TODO: handle exception
			e.printStackTrace();
		}
	}
	
	public static  void methodB() throws Exception{
		Thread.sleep(5000);
		System.out.println("end");
	}
	
	public static void main(String[] args) {
		test t = new test();
		ThreadA threadA = new ThreadA(t);
		threadA.setName("A");
		threadA.start();
		
		ThreadB threadB = new ThreadB(t);
		threadB.setName("B");
		threadB.start();
	}
}
 执行结果:两个线程获取的是不同的对象锁,一个是test.classs对象锁,一个是test对象锁
start not static :A
start:B
static end
end 
  • String常量池类型锁

 在JVM中String有常量池缓存的特性

 什么事常量池?:

  常量池(constant pool)在编译期间被指定,并被保存在已编译的.class文件当中,用于存储关于类、方法、接口中 的常量,也包括字符串直接量

 String与常量池

  String str1 = new String("abc")

  String str2 = "abc";

 上面是两种创建字符串的方式,看起来没有什么区别,但实则有很大区别

 第一种是用new()来新建对象的,它会在存放于堆中。每调用一次就会创建一个新的对象。

 而第二种是先在栈中创建一个对String类的对象引用变量str2,然后通过符号引用去字符串常量池里找有没有"abc",如果没有

,则将"abc"存放进字符串常量池,并令str2指向”abc”,如果已经有”abc” 则直接令str2指向“abc”。

所以如果String str3 = “abc”;str2 和str3是同一对象,String str4 = “adcd”;str4和str2是不同的对象。

结论:如果使用String对象作为对象锁,必须要注意是否对象会改变;这就是String常量池带来的问题,一般不会用String做

为对象锁,而改用其他,比如 new Object()。


  • 死锁

 由于不同的线程都在等待永远不能被释放的锁,从而导致任务不能继续执行。在多线程中死锁是必须避免的,会导致线程的

 假死。

public class test {
	public synchronized void methodA(){
		System.out.println("start:"+Thread.currentThread().getName());
		try {
			Thread.sleep(5000);
			
			while(true){
				
			}
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println("static end");
	}
	
	public static void main(String[] args) {
		test t = new test();
		ThreadA threadA = new ThreadA(t);
		threadA.setName("A");
		threadA.start();
		
		ThreadB threadB = new ThreadB(t);
		threadB.setName("B");
		threadB.start();
	}
}
 执行结果:程序会一直等待。

 我们可以通过jstack 命令来查看jvm内线程状态,来找到死锁的地方。

  • volatile关键字

 volatile关键字的主要作用是使变量在多个线程中可见。

 强制从公共的堆栈中取得变量的值,而不是在线程的私有栈中取变量的值。

 解决同步死循环:

public class RunThread implements Runnable{
	private boolean isRun = true;
	
	public void setIsRun(boolean flag){
		this.isRun = flag;
	}
	@Override
	public void run() {
		// TODO Auto-generated method stub
		while(isRun == true){
			try {
				Thread.sleep(2000);
				System.out.println("当前线程:"+Thread.currentThread().getName());
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		System.out.println("end");
	}
}
public class test {
	public static void main(String[] args) {
		RunThread runthread = new RunThread();
		new Thread(runthread).start();
		System.out.println("我要停止了");
		runthread.setIsRun(false);
		
	}
}

 执行结果:上面代码运行在64bitJVM,-server模式程序陷入死循环。解决办法是使用volatile关键字,从公共堆栈中取得数

据,而不是线程私有栈中

 volatile非原子性

public class MyThread extends Thread{
	volatile private static int count = 0;
	public void run(){
		addCount();
	}
	
	public void addCount(){
		for(int i = 0;i<100;i++){
			count++;
		}
		System.out.println(count);
	}
}
 主类:
public class test {
	public static void main(String[] args) {
		MyThread [] threadArr = new MyThread[100];
		for(int i=0;i<100;i++){
			threadArr[i] = new MyThread();
		}
		
		for(int i =0;i<100;i++){
			threadArr[i].start();
		}
		
	}
}
 执行结果

8500
8900
9100
9204
9298
9398
9498
9598
9698
9798
9898
9998

 关键字volatile主要使用场合是在多线程中可以感知变量值更改了,并且可以获得最新的值,每次取值都是从共享内存中

 取的数据,而不是从线程的私有内存中取得数据。

 但如果修改实例变量,比如i++,这样的操作并不是一个原子操作,也是非线程安全的。表达式的步骤是:

 1)从内存中取得i的值

 2)计算i的值

 3)将i的值写到主存中

 如果在第二步计算值时,其他线程也在修改i的值,就会出现脏读。解决办法是使用synchronized,volatile只能保证从

 每次从主存中取得i的值。所以说volatile并不具有原子性。而是强制数据的读写影响到主存中去。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值