synchronized

0.什么时候会出现线程安全问题?

在单线程中不会出现线程安全问题,而在多线程编程中,有可能会出现同时访问同一个资源的情况,这种资源可以是各种类型的资源,eg,一个变量、一个对象、一个文件、一个数据库表等, 而多个线程同时访问同一个资源的时候,就会存在一个问题,即,由于每个线程执行的过程是不可控的,所以很可能导致的最终结果与实际上的愿望相违背或者直接导致程序出错。

1.如何实现线程安全?

(1)互斥同步

同步是指在多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个(或是一些,使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。

在java中,最基本的互斥同步手段是synchronized关键字,synchronized关键字经过编译之后,会在同步快的前后分别形成monitorenter和monitorexit两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象的锁,如果这个对象没被锁定或者当前线程已经有了那个对象的锁,把锁的计数器加1;执行monitorexit指令时将锁计数器减1;当计数器为0时,锁就被释放。

除了synchronized,可以使用java.util.concurrent包中的重入锁(ReentrantLock)实现同步,在基本用法上,ReentrantLock和synchronized很相似,它们具备一样的线程重入特性,只是代码写法上的区别,一个表现为API层面的互斥锁,另一个表现为原生语法层面的互斥锁。

(2)非阻塞同步

互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步称为阻塞同步。从处理问题的方式上,互斥同步属于一种悲观的并发策略,总认为只要不去做正确的同步措施(例如加锁),那就肯定会出现问题,无论共享数据是否真的会出现竞争,它都要进行加锁。

随着硬件指令集发展,另一个选择:基于冲突检测的乐观并发策略,即先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生的冲突,那就再采取其他的补偿措施(最常见的即不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此成为非阻塞同步。

(3)无同步方案

要保证线程安全,并不一定就要进行同步,两者没有因果关系,同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无需任何同步措施去保证正确性,因此会有一个写代码天生就是现场安全的,比如:可重入代码,线程本地存储。

2.synchronized

synchronized可以修饰方法、代码块,但不能修饰构造器、成员变量等。具体表现为以下三种形式:

  • 对于普通同步方法,锁是当前实例对象
  • 对于静态同步方法,锁是当前类的class对象
  • 对于同步方法块,锁是synchronized括号里配置的对象

(1)同步方法,即有synchronized关键字修饰的方法

java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法,在调用该方法之前,需要获得内置锁,否则就处于阻塞状态

对于synchronized修饰的实例方法(非static方法)而言,无需显式指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象

(2)同步代码块

我们在用synchronized时,能缩小代码段的范围就尽量缩小,能在代码段上加同步就不要在整个方法上加同步,即减小锁的力度,能使代码更大程度并发

//	给方法加上synchronized
	public synchronized void out(String str){
		for(int i=0;i<str.length();i++){
			System.out.print(str.charAt(i));
		}
		System.out.println();
	}
//	同步代码块,this表示当前对象,若传入str表示str对象
	public void out1(String str){
		synchronized(this){
			for(int i=0;i<str.length();i++){
				System.out.print(str.charAt(i));
			}
			System.out.println();
		}
	}
//	out2是静态方法,没有对应类的实例对象依然可以调用
//	静态方法锁不是this,是类的字节码对象,类的字节码对象时优先于类实例对象存在
	public void out1static(String str){
		synchronized(SynTest.class){
			for(int i=0;i<str.length();i++){
				System.out.print(str.charAt(i));
			}
			System.out.println();
		}
	}
	
	public synchronized static void out2(String str){
		for(int i=0;i<str.length();i++){
			System.out.print(str.charAt(i));
		}
		System.out.println();
	}

3.注意

(1)当一个线程正在访问一个对象的synchronized方法,那么其他线程不能访问该对象的其他synchronized方法,因为一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程就无法获取该对象的锁,所以无法访问该对象的其他synchronized方法

(2)当一个线程正在访问一个对象的synchronized方法,那么其他线程能访问该对象的非synchronized方法,访问非synchronized方法不需要获得该对象的锁,假如一个方法没用synchronized关键字修饰,说明它不会使用到临界资源,那么其他线程是可以访问该方法的

(3)如果一个线程A需要访问对象object1的synchronized方法fun1,另外一个线程B需要访问对象object2的synchronized方法fun1,也不会产生线程安全问题,因为他们访问的是不同的对象,所以不存在互斥问题

补充,最近在看《java高并发程序设计》,关于synchronized做点笔记,再次记忆!!

关键字synchronized有很多种用法,简单整理:

  • 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁
  • 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁
  • 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁

代码示例:

将synchronized作用于一个给定对象instance,因此,每次当线程进入被synchronized包裹的代码段,就都会要求请求instance实例的锁,如果当前有其他线程正持有这把锁,那么新到的线程就必须等待

package synchronizedTest;

public class AccountingSync implements Runnable{
	static AccountingSync instance=new AccountingSync();
	static int i=0;
	public void run(){
		for(int j=0;j<1000000;j++){
			synchronized(instance){
				i++;
			}
		}
	}

	public static void main(String[] args) throws InterruptedException {
		// TODO Auto-generated method stub
		Thread t1=new Thread(instance);
		Thread t2=new Thread(instance);
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.out.println(i);
	}
}

上述代码也可写为如下形式,即synchronized作用于一个实例方法

package synchronizedTest;

public class AccountingSync2 implements Runnable{
	static AccountingSync2 instance=new AccountingSync2();
	static int i=0;
	public synchronized void increase(){
		i++;
	}
	public void run(){
		for(int j=0;j<1000000;j++){
			increase();
		}
	}

	public static void main(String[] args) throws InterruptedException {
		// TODO Auto-generated method stub
		Thread t1=new Thread(instance);
		Thread t2=new Thread(instance);
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.out.println(i);
	}
}

在进入increase方法前,线程必须获得当前对象实例的锁。由main方法,使用Runnable接口创建两个线程,并且这两个线程都指向同一个Runnable接口实例(instance对象),这样才能保证两个线程在工作时能够关注到同一个对象锁上去,从而保证线程安全

如下程序则不是线程安全的,因为虽然increase方法为同步方法,但是执行这段代码的两个线程指向了不同的Runnable实例,因此线程t1会在进入同步方法前加锁自己的Runnable实例,而线程t2也关注与自己的对象锁,这两个线程是两把不同的锁,因此线程安全无法保证

package synchronizedTest;

public class AccountingSyncBad implements Runnable{
	static int i=0;
	public synchronized void increase(){
		i++;
	}
	public void run(){
		for(int j=0;j<1000000;j++){
			increase();
		}
	}
	
	public static void main(String[] args) throws InterruptedException {
		// TODO Auto-generated method stub
		Thread t1=new Thread(new AccountingSyncBad());
		Thread t2=new Thread(new AccountingSyncBad());
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.out.println(i);
	}
}

上述代码是否可以修改一下?

当然是的,使用synchronized的第三种用法,即将其作用于静态方法,将上述代码稍作修改,这样即使两个线程指向的是两个不同的Runnable对象,但由于方法块需要请求的是当前类的锁而非当前实例,因此线程间可以正确同步

	public static synchronized void increase(){//synchronized作用于静态方法
		i++;
	}

除了用于线程同步,synchronized还可以保证线程间的可见性和有序性。从可见性角度上讲,synchronized可以完全替代volatile的功能只是使用上没那么方便;就有序性,由于synchronized限制每次只有一个线程可以访问同步块,因袭无论同步块内的代码如何被乱序执行,只要保证串行语义一致,那么执行结果总是一样的。而其他访问线程,又必须在获得锁后方能进入代码块读取数据,因此它们看到的最终结果并不取决于代码的执行过程,从而有序性自然得到解决。

注意,加锁必须合理来加,eg:

package jichu;

public class BadLockOnInteger implements Runnable{
	public static Integer i=0;
	static BadLockOnInteger instance=new BadLockOnInteger();
	public void run(){
		for(int j=0;j<1000000;j++){
			synchronized(i){
				i++;
			}
		}
	}
	public static void main(String[] args) throws InterruptedException {
		// TODO Auto-generated method stub
		Thread t1=new Thread(instance);
		Thread t2=new Thread(instance);
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.out.println(i);
	}

}

该代码和上述示例类似,给计数器加锁,但运行结果可能是984693等,why?

在java中,Integer属于不变对象,即对象一旦被创建就不能被修改,如果你有一个Integer代表1,那么它就永远表示1,你不可能修改Integer的值使它为2,那如果需要让它为2,how?新建一个Integer,并让它表示2

so,上述代码,i++在执行时的真实代码为:

i=Integer.valueOf(i.intValue()+1);

查看源码

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

Integer.valueOf实际上是一个工厂方法,返回一个代表指定数值的Integer实例,因此这里的i++本质即创建一个新的Integer对象,并将它的引用赋给i,so,上述代码中多个线程间,并不一定能看到同一个i对象,因此两个线程每次假设可能都加载了不同的对象实例上而导致对临界区代码控制出现问题

修改,

                       synchronized(instance){
				i++;
//		        i=Integer.valueOf(i.intValue()+1);
			}


参考:

http://tengj.top/2016/05/03/threadsynchronized2/

http://www.cnblogs.com/dolphin0520/p/3923737.html

《深入理解java虚拟机》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值