
相关文章链接
- Java 并发1:线程的基本概念volatile&synchronized关键字
- Java 并发2 :Java中的原子类&互斥 & 线程中异常处理
- Java 并发3:线程通信机制和生产者消费者问题&哲学家就餐问题
因为上一篇文章由于篇幅原因,很多概念塞进去的话会过于臃肿,这篇文章主要分成两个小节,对于上篇文章进行补充:
- 对上篇文章的补充
1.1 ThreadLocal类
1.2 线程中的异常处理 - 互斥的概念
2.1 锁和监视器
2.2 使用可重入锁和AtomInteger
重写上篇文章的例子
上篇文章的补充
上篇文章说明了volatile
和synchronized
关键字的用法,其中volatile
关键字可以保证除了long
和double
类型的的可见性,之所以如此,是因为JVM可以保证的原子操作一般是32位的,而64位的long
和double
需要两次操作,这样就会产生字撕裂
问题。上次内容补充“字撕裂”问题。关于JMM内存模型,可以详细参考这篇文章
ThreadLocal的使用
既然volatile
是为了让属性在线程间访问能够一致,然而线程之间的工作内存却只是主内存的copy,之后还要再刷新回去,线程之间能否拥有一个自己的真正的“私人空间”呢?
我们可以使用ThreadLocal
类
ThreadLocal 例子
public class ThreadLocalVariableHolder {
//虽然是一个static 但是还是每一个线程不相同
private static ThreadLocal<Integer> value = new ThreadLocal<Integer>(){
private Random random = new Random(47);
@Override
protected synchronized Integer initialValue() {
return random.nextInt(10000);
}
};
private static void increment(){
value.set(value.get() + 1);
}
public static int get(){ return value.get(); }
private static class Accessor implements Runnable{
private final int id;
private Accessor(int id){
this.id = id;
}
@Override
public void run() {
while(!Thread.currentThread().isInterrupted()){
ThreadLocalVariableHolder.increment();
System.out.println(this);
Thread.yield();
}
}
public String toString(){
return "#" + id + ": " + ThreadLocalVariableHolder.get();
}
}
public static void main(String args[]) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 5 ; i++) {
executorService.execute(new Accessor(i));
}
try {
TimeUnit.MILLISECONDS.sleep(300);
executorService.shutdownNow();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
ThreadLocal可以使用get()
和set()
方法获取和设置值,初始值需要重写 initialValue()
方法获取。main()
中初始化了5个线程,放入线程池开始运行。随着类而初始化的变量,在Accessor
的run()
方法中被修改。因为ThreadLocal
是每个线程都有的。虽然ThreadLocalVariableHolder
是一个静态变量,但是每个线程中的随机的开始值是不相同的,运行结果如下:
#1: 556
#3: 1862
#2: 6694
#0: 9259
#2: 6695
#4: 962
#3: 1863
#1: 557
#3: 1864
#4: 963
#2: 6696
#0: 9260
#2: 6697
#4: 964
#3: 1865
#1: 558
#3: 1866
#4: 965
#2: 6698
#0: 9261
#2: 6699
.....
线程中的异常处理
如果run()
方法中抛出了没有捕获的异常会怎么样呢?
public static class CaughtThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread() + "--->"+ Thread.currentThread().getUncaughtExceptionHandler());
throw new RuntimeException();
}
}
public static void main(String args[]){
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(new CaughtThread());
executorService.shutdown();
}
控制台会直接返回错误然后结束掉
Exception in thread "pool-1-thread-1" java.lang.RuntimeException
at ExceptionHandlerFactory$CaughtThread.run(ExceptionHandlerFactory.java:20)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
线程使用了这样的一个理念: 线程是一个独立的代码片段,自己的问题不应该交给外部处理。所以我们可以通过这样的方法完成线程的异常处理:
public class ExceptionHandlerFactory implements ThreadFactory {
public static class MyExceptionHandler implements Thread.UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("靠谱的异常捕捉方式 " + e);
}
}
public static class CaughtThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread() + "--->"+ Thread.currentThread().getUncaughtExceptionHandler());
throw new RuntimeException();
}
}
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setUncaughtExceptionHandler(new MyExceptionHandler());
return thread;
}
public static void main(String args[]){
ExecutorService executorService = Executors.newCachedThreadPool(new ExceptionHandlerFactory());
// ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(new CaughtThread());
executorService.shutdown();
}
}
- 自定义一个异常处理类
- 通过实现
ThreadFactory
类中的newThread
方法,给需要处理的线程手动设置好处理类 - 使用工厂方法,需要生成什么样的线程就调用什么样的方法
ExecutorService executorService = Executors.newCachedThreadPool(new ExceptionHandlerFactory());
互斥的概念
临界资源的概念
两个线程同时竞争一个资源,这个资源就称为临界资源。临界资源需要处理得当,否则有可能就会造成死锁,比如哲学家问题中,所有哲学家都拿着左手边的筷子,看着右边的人希望他放下筷子,这就锁住了。
锁 & 对象监视器
- 监视器
监视器是一种实现同步的模型,Java在对象的层面上就对监视器模式进行了支持,详细可以参考Java 监视器模型的实现 - 锁
下面是我对与锁和监视器的理解:
Java中的每一个对象都有一个对象监视器,线程如果需要获取互斥数据的话,需要从监视器手上获取这样的“许可”,这样的许可 我们就称作锁。
上篇文章最后的例子中currentEvenValue
就是一个临界资源。我们常常使用同步的方法可以解决临界资源的访问问题,现在使用Java中的可重入锁之一的ReentrantLock
重写上篇中的问题:
什么叫做可重入?
上面说的哲学家问题就是一个可能会造成死锁的著名问题,请看下面的代码
private static synchronized void testA(){
System.out.println("this is testA");
testB();
}
private static synchronized void testB(){
System.out.println("this is testB");
testA();
}
testA()
调用了 testB()
, testB()
调用了 testA()
这两个方法套娃了,因为synchronized
是一个可重入锁,所以testA()
获取对象上的锁调用testB()
的时候可以成功。可重入锁概括起来就是:如果这个线程已经获取了这个对象上的锁,之后不需要再重复获取。所以Java中的显示锁ReentrantLock
也是一个可重入锁,和synchronized
关键字在可重入上意思相同。
使用可重入锁重写上面的例子
使用同步方法重写的方法:
public class SynchronizedEvenChecker extends IntGenerator{
private int currentEvenValue = 0;
//父类中的方法是同步方法,子类中的方法不一定会继承
@Override
public synchronized int next() {
currentEvenValue++;
Thread.yield(),
currentEvenValue++;
return currentEvenValue;
}
}
使用可重入锁:
private Lock lock = new ReentrantLock();
@Override
public int next() {
lock.lock();
try{
++currentEvenValue;
Thread.yield();
++currentEvenValue;
return currentEvenValue;
}finally {
lock.unlock();
}
}
可以看出来ReentrantLock
的使用很简单,实际上使用ReentrantLock
有这两点好处:
- 可以使用
finally
语句在最后进行解锁操作 -
ReentrantLock
相比synchronized
有更细粒度的控制能力,不需要和对象或者类联系起来
Java中的原子类
CAS操作
CAS操作是Java原子类的核心,CAS操作。CAS 操作是一个原子操作,即CompareAndSwap操作需要接收三个操作数,目标内存地址V,旧值A,待修改的值B。当且仅当V中的值等于A时,将V中的值修改成B。
同样的,这个类我们也可以使用AtomicInteger
重写
private AtomicInteger currentEvenValueAtomic = new AtomicInteger(0);
//使用Java原子类机制引用机制
@Override
public int next() {
return currentEvenValueAtomic.addAndGet(2);
}
addAndGet()
是一个原子操作,这样就不需要加上同步锁了。
这篇文章的内容比较杂,因为有些概念需要理清楚,下篇文章将会涉及到定义在Object中的几个final
方法: wait()
notify()
notifyAll()
以及 哲学家问题的解决。
参考内容:
监视器–Java同步基本概念
锁和监视器的区别
notify 和 notifyAll区别
线程挂起,睡眠,阻塞区别
Thread join 相关用法
读《Thinking in Java有感》,遂记之
网友评论