并发编程的三大特性有:
可见性 、有序性、 原子性
可见性:
/**
* 可见性测试
*/
public class TestThread {
private static /*volatile*/ boolean running = true;
private static void test(){
System.out.println("test start");
while (running){
//System.out.println("hello test ");
}
System.out.println("test end");
}
@SneakyThrows
public static void main(String[] s){
new Thread(TestThread::test,"t").start();
TimeUnit.SECONDS.sleep(1);
running = false;
}
}
上述代码里test方法的线程我期望1秒后停止运行,但实际情况是永远停止不了,因为test()方法感知不到running的值已经变化了,因为running变量是不可见的;上面代码里有两个线程1是main主线程2是t线程,每个线程运行的时候是把running变量拷贝到属于自己的线程缓存里运算,各个线程之间的变量相互不可见(此时没有volatile关键字修饰running变量)
如果把上述代码里的//System.out.println("hello test ");注释放开,正常情况下t线程不会结束,但奇怪的是t线程运行结束了,这是为啥?因为代码System.out.println("hello test ")底层代码其实是加锁了的,加了锁就会触发线程内存数据和主内存数据之间同步刷新数据,此时running变量就变为可见了(running变量在main线程和t线程之间相互可见了)。如果把代码/*volatile*/注释放开,那么running变量也变为可见了。
需要注意的是 volatile 修饰引用数据类型的时候只能保证引用本身的可见性,不能保证引用内部字段的可见性。
有序性:
首先问自己一个问题,程序真的是按照代码顺序执行的吗?不一定。比如int x=0;int y =2;这两行代码执行的顺序是随机的,这就是乱序执行的现象。乱序存在的前提必须是不影响单线程最终一致性,那么执行的语句可以乱序的。但多线程如果乱序估计就会出问题了。
public class TestThread {
private static /*volatile*/ boolean ready = false;
private static int num;
private static class ReaderThread extends Thread{
@Override
public void run(){
while (!ready){
Thread.yield();
}
System.out.println("num=="+num);
}
}
@SneakyThrows
public static void main(String[] s){
Thread t = new ReaderThread();
t.start();
num=42;
ready=true;
t.join();
}
}
上面的代码需要给ready 变量加上修饰volatile 增加可见性,当然你不加可能也是正常的因为yield()方法可能会刷新内存数据同步。
还有就是num的输出值可能是0,因为num=42;ready=true不一定是顺序执行的,可能是乱序执行的,因为你没加volatile修饰num变量,yield()方法可能不会及时刷新内存数据同步。
volatile关键字的两大作用:1.保证变量线程之间可见;2.禁止指令重排序,就是禁止乱序执行。volatile变量规则就是:对一个volatile修饰的变量的写操作先行发生于后面(时间上)对这个变量的读操作。
原子性:
public class TestThread {
private static long n = 0L;
@SneakyThrows
public static void main(String[] s){
Thread[] threads = new Thread[100];
CountDownLatch latch = new CountDownLatch(threads.length);
for (int i = 0;i < threads.length; i++){
threads[i] = new Thread(()->{
for (int j = 0; j < 10000; j++){
//synchronized (TestThread.class){
n++;
//}
}
latch.countDown();
});
}
for (Thread t : threads){
t.start();
}
latch.await();
System.out.println(n);
}
}
上述代码没加Synchroized 之前期望是n=1000000,但实际情况是132065或者其他小于1000000的结果,这就是多线程之间产生了竞争。
上锁就是为了保证操作的原子性;原子性就是不能并发,只能一个线程在干活,不能被打断。上锁的意思就是大挂号{}里面是一个整体不可以被其他线程打断的意思,这保证了n++的原子性;上锁的本质:就是并发编程序列化,多线程逐个的执行,不能并发执行被上锁的代码,A线程执行完了才能执行B线程。
注意:Synchroized 保证了原子性和可见性,但不保证被上锁代码段的有序性。保证原子性不能被打断,说的就是多核cpu的情况多线程执行;如果你是单核cpu就不存在这种情况,或者单线程也不存在这种情况,没人能打断你,就你一个人在干活;
代码上锁分为悲观锁和乐观锁,实际当中就用synchronized就好,现在它已经被优化了,里面既有乐观锁也有悲观锁。