Java多线程核心技术(二):对象及变量的并发访问访问

一、synchronized同步方法

1、方法内的变量为线程安全的

2、实例变量非线程安全:不同的线程访问同一个对象的同一个实例变量是非线程安全的,必须在访问此实例变量的方法上加synchronized关键字。

3、多个对象多个锁:如果多个线程分别访问同一个类的多个不同实例的相同名称的同步方法,会分别对每个实例产生一把锁,效果是以异步的方式运行的。

4、synchronized方法与锁对象:

(1)A线程先持有object对象的Lock锁,B线程可以以异步的方式调用object对象中的非synchronized类型的方法。

(2)A线程先持有object对象的Lock锁,B线程如果在这时调用object对象在中的synchronized类型的方法则需等待,也就是同步。

5、synchronized锁重入

关键字synchronized拥有锁重入的功能,也就是使用synchronized时,当一个线程得到一个对象锁之后,再次请求此对象锁时是可以再次得到该对象的锁的。也就是说在一个synchronized方法/块内部调用本类的其他synchronized方法/块时,是永远可以得到锁的。

class Service{
	synchronized public void service1(){
		System.out.println("service1");
		service2();
	}
	synchronized public void service2(){
		System.out.println("service2");
		service3();
	}
	synchronized public void service3(){
		System.out.println("service3");
	}
}
class MyThread extends Thread{
	@Override
    public void run(){   	
		Service service = new Service();
		service.service1();
    }
}
public class TestClass{
	public static void main(String[] args){		
		MyThread t = new MyThread();
		t.start();
	}
}
程序运行结果如下:

可重入锁也支持在父子类继承的环境中,即在子类synchronized方法中可以调用父类的synchronized方法。

6、出现异常,锁自动释放:当一个线程执行的代码出现异常时,其所持有的锁会自动释放。

7、同步不具有继承性

如下实例代码:

class Main{
	synchronized public void service() {
		try {
			System.out.println("int main threadName = "+Thread.currentThread().getName() + " start time = "+System.currentTimeMillis());			
			Thread.sleep(5000);
			System.out.println("int main threadName = "+Thread.currentThread().getName() + " end   time = "+System.currentTimeMillis());			
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}
class Sub extends Main{
    public void service() {
    	try {
			System.out.println("int sub threadName = "+Thread.currentThread().getName() + " start time = "+System.currentTimeMillis());			
			Thread.sleep(5000);
			System.out.println("int sub threadName = "+Thread.currentThread().getName() + " end   time = "+System.currentTimeMillis());			
			super.service();
    	} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
    }
}
class MyThread extends Thread{
	private Sub sub;
	public MyThread(Sub sub){
		this.sub = sub;
	}
	@Override
    public void run(){   	
		sub.service();
    }
}
public class TestClass{
	public static void main(String[] args){	
		Sub sub = new Sub();
		MyThread tA = new MyThread(sub);
		tA.setName("A");
		tA.start();
		MyThread tB = new MyThread(sub);
		tB.setName("B");
		tB.start();
	}
}
代码运行结果如下:可以看到在sub中的service方法是异步执行的,而其父类的service方法是同步执行的,因此要想sub中的方法异步执行也需要在方法前面 synchronized关键字。



二、synchronized同步语句块

1、sychronized同步代码块的使用
(1)当两个并发线程访问同一个对象object中的synchronized(this)同步代码块时,一段时间内只能有一个线程被执行,另一个线程必须等待当前线程执行完整个代码块以后才能执行该代码块。
(2)在同一个方法中既有非synchronized代码块又有sychronized代码块,则非synchronized代码异步执行,synchronized代码块同步执行。
(3)当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对同一个object中所有其他synchronized(this)同步代码块的访问将被阻塞,因为synchronized(this)是对当前对象进行加锁。如果需求是不同的synchronized代码块可以异步执行,那synchronized加锁的对象可以设定为不同的非this对象。
如下代码是使用synchronized出现的脏读现象:
import java.awt.List;
import java.util.ArrayList;

class MyOneList{
	private ArrayList list = new ArrayList();
	synchronized public void add(String data){
		list.add(data);
	}
	synchronized public int getSize(){
		return list.size();
	}
}
class MyService{
	public MyOneList addServiceMethod(MyOneList list,String data){
		try{
			if(list.getSize() < 1){
				Thread.sleep(2000);
				list.add(data);
			}
		}catch(InterruptedException e){
			e.printStackTrace();
		}
		return list;
	}
}
class MyThread1 extends Thread{
	private MyOneList list;
	public MyThread1(MyOneList list){
		this.list = list;
	}
	@Override
	public void run(){
		MyService msRef = new MyService();
		msRef.addServiceMethod(list, "A");
	} 
}
class MyThread2 extends Thread{
	private MyOneList list;
	public MyThread2(MyOneList list){
		this.list = list;
	}
	@Override
	public void run(){
		MyService msRef = new MyService();
		msRef.addServiceMethod(list, "B");
	} 
}
public class TestClass{
	public static void main(String[] args) throws InterruptedException{	
		MyOneList list = new MyOneList();
		MyThread1 thread1 = new MyThread1(list);
		thread1.setName("A");
		thread1.start();
		MyThread2 thread2 = new MyThread2(list);
		thread2.setName("B");
		thread2.start();
		Thread.sleep(5000);
		System.out.println("listSize="+list.getSize());
	}
}
程序运行后输出listSize=2,但我们的本意是只向MyOneList中添加一个数据,出现这种情况的原因是两个线程以异步的方式返回list参数的size()大小,解决的办法就是“同步化”,如下代码所示:
class MyService{
	public MyOneList addServiceMethod(MyOneList list,String data){
		try{
			synchronized (list) {
				if(list.getSize() < 1){
					Thread.sleep(2000);
					list.add(data);
				}
			}
		}catch(InterruptedException e){
			e.printStackTrace();
		}
		return list;
	}
}

由于list参数对象在项目中是一份实例,是单例的,而且也正需要对list参数的getSize()方法做同步的调用,所以就对list参数进行同步处理。
2、静态同步synchronized方法与synchronized(class)代码块
关键字synchronized还可以应用在static静态方法上,如果这样写就是对当前的*.java文件对应的Class类进行持锁。而synchronized(class)代码块的作用其实和synchronized static方法的作用一样。
3、锁对象的改变
如果锁的对象改变了,那程序的执行就是异步的,但是只有对象不变,即使对象的属性改变了,运行的结果还是同步的。
如下代码是对象改变的实例:
class MyService{
	private String lock = "123";
	public void testMethod(){
		try{
			synchronized (lock){
				System.out.println(Thread.currentThread().getName()+" begin "+System.currentTimeMillis());
				lock="456";
				Thread.sleep(2000);
				System.out.println(Thread.currentThread().getName()+" end "+System.currentTimeMillis());				
			}
		}catch(InterruptedException e){
			e.printStackTrace();
		}
	}
}
public class TestClass{
	public static void main(String[] args) throws InterruptedException{	
		final MyService service = new MyService();
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				// TODO Auto-generated method stub
				service.testMethod();
			}
		},"T1");
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				// TODO Auto-generated method stub
				service.testMethod();
			}
		},"T2");
		t1.start();
		Thread.sleep(50);
		t2.start();
	}
}
代码运行结如下所示:

这说明两个线程是异步执行的,因为在t1执行过程中lock的值改变了,这使得t1和t2加锁的对象不同,所以可以异步执行。

三、volatile关键字

关键字volatile的主要作用是使变量在多个线程间可见,使用它可强制从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量的值。

1、关键字synchronized和volatile的比较

(1)关键字volatile是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好,并且volatile只能修饰于变量,而synchronized可以修饰方法,以及代码块。

(2)多线程访问volatile不会发生阻塞,而synchronized会出现阻塞。

(3)volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,可以间接保证可见性,因为他会将私有内存和公共内存中的数据做同步。

(4)volatile解决的是变量在多个线程之间的可见性;而synchronized关键字解决的是多个线程之间访问资源的同步性。

2、volatile非原子的特性

如果修改实例变量中的数据,比如i++,也就是i=i+1,则这样的操作其实并不是一个原子操作,也就是非线程安全的。表达式i++的操作步骤分解如下:

(1)从内存中取出i的值;

(2)计算i的值;

(3)将i的值写到内存中。

假如在第2步计算值的时候,另外一个线程也修改i的值,那么这个时候就会出现脏数据。解决的办法就是使用synchronized关键字。所以说volatile本身并不处理数据的原子性,而是强制对数据的读写及时影响到主存。

变量在内存中工作的过程如下:

(1)read和load阶段:从主存复制变量到当前线程工作内存;

(2)use和assign阶段:执行代码,改变共享变量值;

(2)store和write阶段:用工作内存数据刷新主存对应变量的值。

在多线程环境中,user和assign是多次出现的,但这一操作并不是原子性,也就是在read和load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,也就是私有内存和公共内存中的变量不同步。

3、使用原子类进行i++操作

除了在i++操作时使用synchronized关键字实现同步外,还可以使用AtomicInteger原子类进行实现。

原子操作是不能分割的整体,没有其他线程能够中断或检查正在原子操作中的变量。一个原子(atomic)类型就是一个原子操作可用的类型,它可以在没有锁的情况下做到线程安全。

4、synchronized代码块有volatile同步的功能

关键字synchronized可以保证在同一时刻,只有一个线程可以执行某一个方法或某一个代码块。它包含两个特征:互斥性和可见性。同步synchronized不仅可以解决一个线程看到对象处于不一致的状态,还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护之前所有的修改效果。

5、volatile变量的使用时机:

当且仅当满足以下所有条件时,才应该使用volatile变量:

A、当对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。

B、该变量不会与其他状态变量一起纳入不变性条件中。

C、在访问变量时不需要加锁。

四、不可变性

不可变对象:如果某个对象在被创建后其状态就不能被修改,那么这个对象就称为不可变对象。不可变对象一定时线程安全的。

当满足一下条件时,对象才是不可变的:

A、对象创建以后期状态就不能修改

B、对象的所有域都是final类型。

C、对象是正确创建的(在对象的创建期间,this引用没有逸出)。

1、Final域

final类型的域是不能修改的(但如果final域所引用的对象是可变的,那么这些被引用的对象是可以修改的)。final域能确保初始化过程的安全性,从而可以不受限制地访问不可变对象,并在共享这些对象时无须同步。

每当需要对一组相关数据以原子方式执行某个操作时,就可以考虑创建一个不可变的类来包含这些数据,如下实例中,BigInteger[]数组 lastFactors中存放了lastNumber因式分解的结果,他们是一对一的关系,而且对它们的操作都是原子的。

class OneValueCache{
	private final BigInteger lastNumber;
	private final BigInteger[] lastFactors;
	
	public OneValueCache(BigInteger i,BigInteger[] factors){
		lastNumber = i;
		lastFactors = factors;
	}
	
	public BigInteger[] getFactors(BigInteger i){
		if(lastNumber == null || !lastNumber.equals(i))
			return null;
		else
			return Arrays.copyOf(lastFactors, lastFactors.length);
	}
}

2、不可变对象与初始化安全性

Java内存模型为不可变对象的共享提供了一种特殊的初始化安全性保证。对于可变对象,即使某个对象的引用对其他线程是可见的,也并不意味着对象状态对于使用该对象的线程来说一定是可见的,为了确保对象状态能呈现出一致的视图,就必须使用同步。而对于不可变对象,即使在发布不可变对象的引用是没有使用同步,也仍然可以安全地访问该对象,在没有额外同步的情况下,也可以安全地访问final类型的域,但是,如果不可变对象中final类型的域所指向的是可变对象,那么在访问这些域所指向的对象的状态时仍然需要同步。

3、可变对象安全使用的常用模式

可变对象必须通过安全的方式来发布,这通常意味着在发布和使用该对象的线程时都必须使用同步。

要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全地发布:

A、在静态初始化函数中初始化一个对象引用

B、将对象的引用保存到volatile类型的域或者AtomicReferance对象中

C、将对象的引用保存在某个正确构造对象的final类型域中

D、将对象的引用保存到一个由锁保护的域中

线程安全库中的容器类提供了以下安全发布的保证:

A、通过将一个键或值放入Hashtable、synchronizedMap或者ConcurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程

B、通过将某个元素放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或者synchronizedSet中,可以安全地将它发布给任何从这些容器中访问它的线程

C、通过将某个元素放入BlockingQueue或者ConcurrentLinkedQueue中,可以安全地将它发布给任何从这些队列中访问它的线程

4、事实不可变对象

如果对象从技术上来看是可变的,但其状态子啊发布后不会在改变,那么把这种对象称为“事实不可变对象”。在没有额外同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。

总结:

在并发程序中使用和共享对象时,可以使用如下策略:

线程封闭:线程封闭的对象只能有一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。

只读共享:在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。

线程安全共享:线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步同步。

保护对象:被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。






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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值