java并发理论基础

一、线程

1、进程与线程

进程:程序的一次执行过程,是系统运行程序的基本单位。启动一个main函数就是启动一个进程

线程:一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程,他们有自己独立的程序计数器、虚拟机栈和本地方法栈,但共享进程的堆和方法区资源

2、线程创建

其实main方法的执行,就已经是运行了多个线程了,包括(1)分发处理发送给JVM信号的线程;(2)调用对象finalize方法的线程;(3)清楚refrence的线程;(4)main线程

那么如何在main中再新建线程,有以下4种方法:

(1)通过继承Thread类,重写run方法;

(2)通过实现runable接口;

(3)通过实现callable接口,需要配合使用线程池

//全部采用匿名内部类的方式实现,也可以单独写一个类继承自Thread类或者实现Runnable接口
public static void main(String[] args) {
    //1.继承Thread
    Thread thread1 = new Thread() {
        @Override
        public void run() {
            System.out.println("继承Thread");
            super.run();
        }
    };

    //2.实现runable接口
    Thread thread2 = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("实现runable接口");
        }
    });

    //3.实现callable接口,配合线程池使用
    ExecutorService service = Executors.newSingleThreadExecutor();
    Future<String> future = service.submit(new Callable() {
        @Override
        public String call() throws Exception {
            return "实现Callable接口";
        }
    });


    thread1.start();
    thread2.start();
    try {
        String result = future.get();
        System.out.println(result);
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
}

结果:

image.png

优缺点对比:

(1)继承Thread类的方式写法比较简单。但Thread是类,只能被继承,当一个类要实现多线程时,如果继承了Thread类,就不能在继承别的类了。并且,thread.start()只能同时调用一次,所谓的多线程,也只是在主线程以外,建立一个线程。

(2)Runnable、Callable接口可以多实现,并且可以启动多个相同的线程处理一个数据,,,,如果需要访问当前线程,必须使用Thread.currentThread()方法

如果用:

image.png

报错:

image.png

但是实现Runnable接口的thread2可以多个同时用:

image.png

结果:

image.png

(3)Callable接口要实现的方法是T  call()  throws Exception,不是void run(),注意可以有返回值,也可以抛出异常(重写Runnable的run方法不能有异常出现)

image.png

最重要的是:运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务的执行情况,可取消任务的执行,还可获取执行结果

3、生命周期和状态转换

image.png

刻意对线程加锁,而不是抢占资源时,线程会进入WAITING或者TIMED_WAITING状态,而不是BLOCKED状态

image.png

4、守护线程

守护线程是一种特殊的线程,是系统的守护者,在后台默默地守护一些系统服务,比如垃圾回收线程,JIT线程就可以理解守护线程。

与之对应的是用户线程,即工作线程,即完成某个业务的线程,当用户线程全部结束后,意味着没有线程需要守护,那么守护线程就会退出,JVM跟着退出

public static void main(String[] args) {
    Thread daemonThread = new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                try {
                    System.out.println("i am alive");
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println("finally block");
                }
            }
        }
    });
    daemonThread.setDaemon(true);
    daemonThread.start();
    //确保main线程结束前能给daemonThread能够分到时间片
    try {
        Thread.sleep(800);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

结果:

image.png

分析:

(1)要将线程设置为守护线程,需要在start()之前调用setDaemon(true);如果放在start之后调用,会报异常,但是程序不会停止,只会当做用户线程执行。

(2)main(用户)线程结束后,没有线程需要守护,守护线程自动退出。

(3)守护线程在退出时不会执行finally块,因为没有用户线程在运行,无论守护线程处于什么状态(阻塞、运行。。。),守护线程直接被kill掉,不再执行任何语句;故释放资源的操作等不要放在finally块中。

5、线程的中断

通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。

例如:Thread.sleep()方法为什么会抛出InterruptedException,就是因为sleep过程中,一旦其他线程调用了这个线程的interrupt()方法,该线程就会被中断,那就会抛出这个异常。

二、多线程

1、并发VS并行

并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进行”。

实际上,如果系统内只有一个CPU,而使用多线程时,那么真实系统环境下不能并行,只能通过切换时间片的方式交替进行,而成为并发执行任务。真正的并行也只能出现在拥有多个CPU的系统中。

2、使用多线程的优缺点

优点:

(1)充分利用多核CPU的计算能力,提高CPU使用率

(2)异步执行任务

(3)并行计算提升应用性能

缺点:

(1)频繁的上下文切换:CPU分配给各个线程的时间片非常短,CPU通过不断切换线程来让用户觉得是多线程同时运行,但切换线程会损耗性能(保存当前状态以便恢复)

(2)会有线程安全问题

3、线程间的协作

(1)join():在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。

(2)wait() notify() notifyAll():

调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。

wait() 挂起期间,线程会释放锁

它们都是 Object类 的方法

(3)await() signal() signalAll():

JUC包中的Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活

Condition condition = lock.newCondition();
condition.await();
condition.signalAll();

(4)Thread.sleep()

与Object.wait()比较:

共同点:都可以暂停线程

区别:

a. Object.wait()释放锁,Thread.sleep()没有释放锁

b. Object.wait() 通常被用于线程间交互/通信,Thread.sleep()通常被用于暂停执行。

c. Object.wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 Object.notify()或者 Object.notifyAll() 方法。Thread.sleep()方法执行完成后,线程会自动苏醒,或者也可以使用 Object.wait(long timeout) 超时后线程会自动苏醒,毕竟Thread也是一个Object,也有wait方法。

d. Thread.sleep() 是 Thread 类的静态本地方法,Object.wait() 则是 Object 类的本地方法

sleep() 方法定义在 Thread 中原因:只是让线程暂停,不涉及对象的操作

wait()方法定义在Object中原因:目的是释放线程占用的锁,而锁其实是对象锁,可以不涉及线程

4、线程不安全示例:

public static void main(String[] args) throws InterruptedException {
    final int threadSize = 1000;
    ThreadUnsafeExample example = new ThreadUnsafeExample();
    final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < threadSize; i++) {
        executorService.execute(() -> {
            example.add();
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    executorService.shutdown();
    System.out.println(example.get());
}

public class ThreadUnsafeExample {

    private int cnt = 0;

    public void add() {
        cnt++;
    }

    public int get() {
        return cnt;
    }
}

最终执行结果一定小于1000

5、线程不安全根本原因

(1)可见性:CPU缓存引起

一个线程对共享变量的修改,另外一个线程能够立刻看到。

(2)原子性:分时复用引起

一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行

(3)有序性:指令重排序引起

在执行程序时为了提高性能,编译器和处理器常常会对指令按如下顺序做重排序

a.编译器优化的重排序:在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序

b.指令级并行的重排序:处理器采用指令级并行技术将多条语句重叠执行(改变语句对应机器指令的执行顺序),前提是不存在数据依赖性

c.内存系统的重排序:处理器使用缓存和读写缓冲区,使得加载和存储操作看上去是乱序执行的

解决:JMM编译器重排序规则会禁止特定类型的编译器重排序;添加内存屏障指令禁止特定的处理器指令重排序,可以使用volatile

6、如何解决并发问题

(1)通过关键字synchronized、volatile、final解决

synchronized解决原子性、可见性、有序性

volatile解决可见性

final使得对象不可变

(2)happens-before原则:解决有序性

八大原则

1. 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。

2. 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。

3. volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。

4. 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。

5. start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。

6. join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。

7. 程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。

8. 对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。

7、线程安全的实现方法

(1)互斥同步

使用synchronized或reentrantlock加锁

(2)非阻塞同步

CAS、Atomic原子操作,其实Atomic底层也是CAS

(3)无同步

使用线程局部变量、ThreadLocal来实现变量的线程隔离,即不存在多线程的情况

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值