JUC并发编程

多任务:一边吃饭一边睡觉

线程(thread):本质上是程序里不同的执行路径

进程(process):由不同的线程组成

一、使用多线程的方法

1.实现 Runnable 接口

先继承Runnable接口创建一个子线程

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        // ...
    }
}

在主线程中使用Thread启动线程

public static void main(String[] args) {
    MyRunnable instance = new MyRunnable();
    Thread thread = new Thread(instance);
    thread.start();
}

2.实现Callable接口

与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装

public class MyCallable implements Callable<Integer> {
    public Integer call() {
        return 123;
    }
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
    MyCallable mc = new MyCallable();
    FutureTask<Integer> ft = new FutureTask<>(mc);
    Thread thread = new Thread(ft);
    thread.start();
    System.out.println(ft.get());
}

3.继承Thread类

 Thread 类也实现了 Runable 接口,所以需要实现 run() 方法

public class MyThread extends Thread {
    public void run() {
        // ...
    }
}
public static void main(String[] args) {
    MyThread mt = new MyThread();
    mt.start();
}

由于java不支持多继承,但是能实现多个接口,所以尽量用接口做多线程

二.线程的状态

Thread.State()中定义了6个状态

public enum State {
       
        NEW,  // 新建,start之前

       
        RUNNABLE,   // 运行

       
        BLOCKED,    // 阻塞,等待获得锁

       
        WAITING,     // 等待,调用了Oject.wait(),Thread.join(),LockSupport.park()

       
        TIMED_WAITING,   // 限时等待,调用了Thread.sleep(time),Oject.wait(time),Thread.join(),LockSupport.parkNanos(),LockSupport.parkUntil()
        TERMINATED; // 终止,线程结束或产生异常
}

锁的分类

1. 乐观锁,悲观锁 :乐观锁认为获取锁的可能性较大,首先尝试获得,如果获得不了就去排队,悲观锁直接排队

2. 独占锁,共享锁:一个典型的例子是读写锁,读的时候允许多个线程访问,写的时候只允许一个线程

3. 可重入锁,不可重入锁:如果一个方法加了锁,其中又调用了另一个加锁的方法,可重入锁允许调用,不重入锁不允许, synchronized是可重入锁。

4. 公平锁,非公平锁:公平锁要求所有线程排队,非公平锁不要求

synchronized,volitile和cas的底层实现完全一样

cas的c++底层代码是lock cmpxchg这条汇编指令,cmpxchg(compare and exchange)并不能保证原子性,因为这条指令的执行过程是拿到值、对值加减、比较值和拿的时候比有无变化,没有变化的话对值修改。如果在比较前值被修改了,这条指令可以起作用,但是如果在比较过程中,对值修改前,值被其他线程修改,这条指令是不能发现的。这个时候靠lock指令保证原子性,lock指令的作用是拿到值到最终修改前,这个过程不允许被打断。

在了解synchronize的原理前需要了解对象布局:

一个对象的内存布局由三个部分组成

                                                                                                             

对象头:包括markword和类型指针,实例数据是对象中的字段,如果对象头加实例数据的大小是8字节的整数倍,由对齐填充部分补齐。

对于Object o = new Object();这条命令,首先在堆里划分一块内存装Object对象,栈里创建一个引用o指向这个对象,这个很简单,关键是堆内存里的Object有什么,可以通过在maven里添加JOL(java object layout)依赖查看对象的内存布局:

 可以看到前12字节是对象头,因为没有字段,所以没有实例数据,对齐填充用4个空字节把对象补齐成16位。

如果对o上锁:synchronize(o)重新查看内存布局

 

可以看到对象头的markword发生了变化,说明锁是加在对象头的markword上的。

实际上markword记录了synchronize锁的升级过程

synchronized从代码到底层实现:1.java代码中使用synchronized2.字节码中会加入monitorenter和monitorexit,线程在获取锁的时候,实际上就是获得一个监视器对象(monitor) ,monitor 可以认为是一个同步对象,线程运行到monitorenter时会尝试获取对象的monitor所有权。3.执行过程中自动升级。4.汇编层使用lock comxchg

synchronized的锁升级:1.无锁2.偏向锁3.自旋锁4.重量级锁。

自旋锁和重量级锁的适用场景:由于自旋锁消耗cpu,所以自旋锁适合锁同步的代码块执行速度快且锁竞争不激烈的情况,重量级锁适合锁同步的代码块执行速度慢且锁竞争激烈的情况

volatile解决指令重排序的过程:1.java代码中对变量使用volatile。2.字节码中添加ACC_VOLATILE。3.JVM使用内存屏障,包括LoadLoad,LoadStore,StoreStore,StoreLoad,Load是读操作,Store是写操作,LoadLoad是指两个在两个读操作之间加屏障,使它们不能交换顺序,其他三个同理。在对volatile进行写操作时,在这条指令前加StoreStore,在后面加StoreLoad,表示全部写完之后才能读。在对volatile进行读操作时,在这条指令前加LoadLoad,在后面加LoadStore,表示全部读完之后才能写。4.hotspot实现:还是用lock

                                                         

 三、AQS

1.CountDownLatch

CountDownLatch是一个倒计数器,用于某个线程等待特定数量的线程执行完毕。但是只能用一次

public class CountdownLatchTest {
    private static final int num = 10;
    public static void main(String[] args) throws InterruptedException {

        CountDownLatch countDownLatch = new CountDownLatch(num);           // 设置CountDownLatch初始值
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for(int i = 0; i < num; i++){
            executorService.execute(() -> {System.out.println("run");
                countDownLatch.countDown();       // 初始值减1
            });

        }
        countDownLatch.await();           // 如果减到0,执行后边的程序
        System.out.println("end");
        executorService.shutdown();
    }
}

2.CyclicBarrier

与CountDownLatch功能类似,但是又几点不同:1.CountDownLatch只能用一次,CyclicBarrier可以通过reset()方法重复使用2.CyclicBarrier.await()方法在检查计数器时,如果大于零会自动减1,因此CyclicBarrier不需要countDown方法。3.基于第二点,CountDownLatch与CyclicBarrier相比,CountDownLatch可以人为控制,在一个线程里可以使用多次countDown方法,使计数器每次减去大于1的值。

public class CyclicBarrierTest {
    public static void main(String[] args){
        final int num = 10;
        CyclicBarrier cyclicBarrier = new CyclicBarrier(num);
        ExecutorService executorService = Executors.newFixedThreadPool(num);
        for(int i = 0; i<num; i++){
            executorService.execute(()->{
                System.out.println("before");
                try {
                    cyclicBarrier.await();          // 检查计数器并减1
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
                System.out.println("after");
            });
        }
    }
}

3.Semaphore

信号量,用于限流,控制同时运行的线程数量,设置一个信号量最大值,线程通过acquire方法获得一个信号量并使信号量减1,线程运行完毕后通过release方法释放信号量

public class SemaphoreTest {
    public static void main(String[] args){
        final int maxThreadNum = 3;         // 最多允许三个线程同时运行
        final int curThreadNum = 10;        //  共有10个线程
        ExecutorService executorService = Executors.newFixedThreadPool(curThreadNum);
        Semaphore semaphore = new Semaphore(maxThreadNum);
        for(int i = 0; i < curThreadNum; i++){
            executorService.execute(()->{
                try {
                    semaphore.acquire();         // 获取信号量,如果没有获取成功则等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(semaphore.availablePermits()+" ");
                semaphore.release();            // 执行完毕,释放信号量
            });
        }
        executorService.shutdown();
    }
}

 

posted @ 2020-03-08 18:25  嫩西瓜  阅读(220)  评论(0)    收藏  举报