java 多线程编程系列之6----并发编程的三大特性

        并发编程的三大特性有:

        可见性 、有序性、 原子性

        可见性:

/**
 * 可见性测试
 */
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就好,现在它已经被优化了,里面既有乐观锁也有悲观锁。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值