Java_多线程_同步

通过线程池同时执行多个线程,这个时候需要特别注意共享变量的并发访问,如果不做处理,很容易出现线程安全问题。

Java通过synchronized关键字支持线程同步,可用于同步方法或者同步代码块。

当一个共享变量被volatile修饰时,它会保证修改的值会立即被保存到主存,当有其他线程需要读取时,它会去内存中读取新值。

package cn.thread4;

/**
 * 测试volatile
 * 保持数据可见性(同步),但不保持原子性(读取-操作-返回)
 * 现很少使用,因为机器性能高,很少出现cpu忙不过来的情况
 * @author Chill Lyn
 *
 */
public class TestVolatile {
	private static volatile int num = 0;

	public static void main(String[] args) throws InterruptedException {
		new Thread(() -> {
			// 死循环保持cpu忙碌状态
			while (num == 0) {

			}
		}).start();
//		一秒后更改数值,循环停止
		Thread.sleep(1000);
		num = 1;
	}
}


而普通共享变量不能保证可见性,因为其被修改后,什么时候被写入主存是不确定的,当其他线程读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

Java在内部使用所谓的监视器monitor,也称为监视器锁monitor lock或内在锁intrinsic lock来管理同步。监视器绑定在对象上。例如,当使用同步方法时,每个方法都共享相应对象的相同监视器。所有的隐式监视器都实现了重入reentrant特性。重入的意思是锁绑定在当前线程上。线程可以安全地多次获取相同的锁,而不会产生死锁。(例如同步方法调用相同对象的另一个同步方法时)

并发API支持多种显式锁,它们由Lock接口规定,用于替代synchronized的隐式锁。锁对细粒度的控制提供多种方法,因此比隐式监控器具有更大开销

ReentrantLock是互斥锁,与通过synchronized访问的隐式监视器具有相同行为,但是具有扩展功能,就像它的名称一样,这个锁实现类重入特性,就是隐式监控器一样。

package com.test.lock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Arith {
	
	private int number = 0;
	
	// 创建一个锁对象
	private Lock lock = new ReentrantLock();
	
	public void increament() {
		lock.lock(); // 锁住对象
		try {
			number ++;
		}finally {
			lock.unlock(); // 释放锁
		}
	}
	public int getNumber() {
		return number;
	}
}
package com.test.lock;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class Test {
	
	public static void main(String[] args) {
		ExecutorService service = Executors.newCachedThreadPool();
		Arith arith = new Arith();
		Runnable task = new Runnable() {
			@Override
			public void run() {
				arith.increament();
			}
		};
		
		for(int i=1;i<=10000;i++) {
			service.submit(task);
		}
		
		service.shutdown();
		try {
			boolean b = service.awaitTermination(60, TimeUnit.DAYS);
			if(b) {
				System.out.println("执行结果:"+arith.getNumber());
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

锁可以通过lock()来获取,通过unlock()释放。吧代码包装在try-finally代码块中来确保异常情况下的解锁非常重要。这个方法时线程安全的,就像同步副本那样。如果另一个线程已经拿到锁了,再次调用lock会阻塞当前线程,直到锁被释放。在任意给定时间内,只有一个线程可以拿到锁。tryLock方法时lock的替代,它尝试拿锁而不阻塞当前线程。

ReadWriteLock接口规定了锁的另一种概念,包含用于读写访问的一对锁。读写锁的理念是,只要没有任何线程写入变量,并发读取变量通常是安全的。所以读锁可以同时被多个线程持有,只要没有线程持有写锁。这样可以提升性能和吞吐量,因为读取比写入更频繁。

package com.test.lock;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockTest {
	
	public static void main(String[] args) {
		ExecutorService service = Executors.newCachedThreadPool();
		Map<String, String> map = new HashMap<String, String>();
		ReadWriteLock lock = new ReentrantReadWriteLock();
		
		Runnable write = new Runnable() {
			@Override
			public void run() {
				lock.writeLock().lock(); // 获取写锁
				try {
					System.out.println(Thread.currentThread()+"-->写入数据");
					map.put("home", "shanghai");
					try {
						Thread.sleep(5000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}finally {
					lock.writeLock().unlock();
				}
			}
		};
		Runnable read = new Runnable() {
			@Override
			public void run() {
				lock.readLock().lock();
				try {
					System.out.println(Thread.currentThread()+"-->"+map.get("home"));
					System.out.println(Thread.currentThread()+"---读取完毕");
				}finally {
					lock.readLock().unlock();
				}
			}
		};
		service.submit(read);
		service.submit(read);
		service.submit(write);
	}
}

上面的例子在暂停5秒之后,首先获取写锁来向map添加新值。在这个任务完成之前,另一个任务被启动,尝试读取map中的元素,当执行这一代码示例时,你会注意到读任务需要等待写任务完成。在释放了写锁之后,读任务才会执行,并且同时打印结果。它们不需要相互等待完成,因为读锁可以安全同步获取,只要没有其他线程获取了写锁。

Java8自带了一种新锁,StampedLock,它同样支持读写锁。与ReadWriteLock不同的,StampedLock的锁方法会返回表示为Long的标记。可以使用这些标记来释放锁,或者检查锁是否有效。此外StampedLock支持乐观锁optimistic locking模式。

StampedLock lock=new StampedLock();
...
long stamp=0;
try{
	stamp=lock.writeLock();
}finally{
	lock.unlockWrite(stamp);
}
...

long stamp=0;
try{
	stamp=lock.readLock();
}finally{
	lock.unlockRead(stamp);
}

通过readLock()或readLock()来获取读锁或写锁会返回一个标记,它可以在稍后用于在finally中解锁。要记住StampedLock并没有实现重入特性。每次调用加锁都会返回一个新的标记,并且在没有可用的锁时阻塞,即使相同线程已经拿到锁了。所以需要额外注意不要出现死锁。

就像前面的ReadWriteLock例子那样,两个读任务都需要等待写锁释放。之后两个读任务同时在控制台打印信息,因为多个读操作不会相互阻塞,只要没有线程拿到写锁。

乐观的读锁通过调用tryOptimisticRead()获取,它总是返回一个标记而不阻塞当前线程,无论锁是否真正可用,如果已经有写锁被拿到,返回的标记为0。总是需要通过lock.validate(stamp)检查标记是否有效

long stamp=lock.tryOptimisticRead();
try{
	System.out.println(lock.validate(stamp));
	sleep(1000);
	System.out.println(lock.validate(stamp));
}finally{
	lock.unlock(stamp);
}

乐观锁在刚刚拿到锁之后是有效地。和普通的读锁不同的是,乐观锁不阻止其他线程同时获取写锁。在第一个线程暂停1秒之后,第二个线程拿到写锁而无需等待乐观锁被释放。此时,乐观的读锁就失效了。甚至当写锁被释放时,乐观的读锁仍然处于无效状态。所以在使用乐观锁时,需要每次在访问任何共享变量之后都要检查锁,来确保读锁仍然有效。有时,将读锁转换为写锁而不用再次解锁和加锁十分实用。StampedLock为这种目的提供了tryConvertToWriteLock(stamp)方法:

long stamp=lock.readLock();
try{
	stamp=lock.tryConvertToWriteLock(stamp);
	if(stamp==0L){
		System.out.println("cannot convert");
		stamp=lock.writeLock();
	}
}finally{
	lock.unlock(stamp);
}
发布了359 篇原创文章 · 获赞 26 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/Chill_Lyn/article/details/104145798