线程基础和等待唤醒机制(三)

一、volatile

public class VolatileTest {

    private static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        // 两个线程对一个变量同时做累加操作
        Thread runningThread = new Thread(new RunningThread(), "runningThread");
        Thread changeThread = new Thread(new ChangeThread(), "changeThread");
        runningThread.start();
        // 先让runningThread运行一会
        Thread.sleep(1000);
        changeThread.start();
    }

    public static void change() {
        flag = false;
    }

    static class RunningThread implements Runnable {
        @Override
        public void run() {
            while (flag) {
            }
            System.out.println(Thread.currentThread().getName()+"跳出寻魂结束运行");
        }
    }

    static class ChangeThread implements Runnable {
        @Override
        public void run() {
            flag = false;
            System.out.println(Thread.currentThread().getName()+"把flag修改成"+flag);
        }
    }
}

打印结果(runningThread线程会一直空转):changeThread把flag修改成false

按照正常思维runningThread线程应该能感知到flag的修改,但是程序就一直卡在那里,就证明runningThread线程对changeThread线程修改的变量是不可见的,这时候加个volatile关键字就可以解决问题,volatile不能保证不是原子操作自增,只能保证单个变量的读/写具有原子性,volatile还可以防止指令重排,保证可见性

二、synchronized对象锁

public class SyncTest {

    private static int value = 0;

    public static void main(String[] args) throws InterruptedException {
        // 两个线程对一个变量同时做累加操作
        Thread thread1 = new Thread(new SyncThread());
        Thread thread2 = new Thread(new SyncThread());
        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println(value);
    }

    static class SyncThread implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                value++;
            }
        }
    }
}

上述代码不管怎么执行,绝大多数情况打印的不会是20000,这是为什么?因为在两个线程同时操作value++,就算加了volatile也达不到想要的结果,而且自增不是原子操作,根据缓存一致性协议,只能保证线程本地缓存的一致性,也就是说加了volatile的属性,线程在读取时,线程本地缓存会失效,由于写操作自增后放到本地缓存,接着刷回主内存,这时候也有可能覆盖其它线程刷回主内存的值,达不到期望结果,可以使用synchronized来控制同一时间只能有一个线程对共享变量的修改,而synchronized加锁的地方不同,下面做一下区分

线程基础和等待唤醒机制(三)_第1张图片

synchronized使用时切记要锁住的是同一个对象,否则和没加锁一样;举例其它情况,用Integer作为锁对象时,如果又对Integer做自增操作,底层会调用Integer.valueOf()创建一个新的Integer对象,这个时候就不是原来的锁对象了

三、wait/notify

当一个线程依赖另一个线程时,共享变量来控制线程的条件,不可能循环去读取,这样长时间占用cpu和锁的资源耗费性能,就可用到wait/notify

public class TestWN {
	
	private static List list = new ArrayList();
    
	static class ConsumerThread implements Runnable {

		@Override
		public void run() {
			try {
				synchronized (list) {
					while (true) {
						if (list.size() == 0) {
							System.out.println("list没有可消费的对象了");
							list.wait();
						}
						System.out.println("ConsumerThread正在消费"+list.get(list.size()-1));
						list.remove(list.size()-1);
						list.notify();
					}
				}
			}catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	
	static class ProducerThread implements Runnable {

		@Override
		public void run() {
			try {
				synchronized (list) {
					while (true) {
						if (list.size() > 0) {
							list.wait();
						}
						System.out.println("ProducerThread创建");
						list.add("ProducerThread");
						list.notify();
					}
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	
	
	public static void main(String[] args) throws InterruptedException {
		new Thread(new ConsumerThread()).start();
		new Thread(new ProducerThread()).start();
		
	}
}

notify()是唤醒单个单线程,notifyAll()是唤醒所有wait()的线程,但是不能指定唤醒哪个线程

为什么应该在循环中检查等待条件?

处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。因此,当一个等待线程醒来时,不能认为它原来的等待状态仍然是有效的,在notify()方法调用之后和等待线程醒来之前这段时间它可能会改变。这就是在循环中使用wait()方法效果更好的原因(因为线程是并发执行的,资源是有限的,唤醒的不一定就能抢到资源,这种虚假唤醒通过while条件判断,这次条件不满足还可以继续等待,不至于没抢到就直接结束线程

为什么wait和notify方法要在同步块中调用?

如果没有加synchronized关键字或者锁的不是同一个对象,就会出现lost wake up情况,假设生产者进行加1操作之后通知消费者,消费者减1操作后进行等待,如果没有同步块限制,而线程又是并发执行的,这时候消费者和生产者容易混在一起执行,比如消费者线程满足while条件判断,但还未进行wait()方法调用,这个时候发生了线程上下文切换,切到生产者线程,满足while条件后进行加1操作后唤醒线程,而消费者线程此时抢到时间片执行就会陷入等待,没有线程再去唤醒它

线程基础和等待唤醒机制(三)_第2张图片

CompletableFuture

JDK1.8才新加入的一个实现类CompletableFuture,实现了Future, CompletionStage两个接口。实现了Future接口,意味着可以像以前一样通过阻塞或者轮询的方式获得结果。

public class CFDemo {

    static class GetResult extends Thread {
        CompletableFuture f;
        GetResult(String threadName, CompletableFuture f) {
            super(threadName);
            this.f = f;
        }
        @Override
        public void run() {
            try {
                System.out.println(this.getName() + ": " + f.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws Exception {
        final CompletableFuture f = new CompletableFuture<>();

        new GetResult("Client1", f).start();
        new GetResult("Client2", f).start();
        System.out.println("sleeping");
        TimeUnit.SECONDS.sleep(2);
        f.complete(100);
//        f.completeExceptionally(new Exception());
    }
}

你可能感兴趣的:(并发,java,开发语言)