java-多线程

概览

  • 基础概念
    • 基本概念解释
    • 线程状态
  • 线程
    • 创建线程
    • Thread中的start()和run()的区别
    • 线程如果遇到异常
  • 线程同步
    • synchronized
      • suspend() resume() stop()(废弃)
      • wait() notify() notifyAll()
      • sleep()和wait()比较
    • ReentrantLock
    • sychronized和ReentrantLock
    • 死锁
    • 乐观锁和悲观锁
    • 偏斜锁、自旋锁、轻量级锁、重量级锁
  • 线程间数据共享
  • 线程池
  • concurrent包
    • ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteArraySet
    • atomic
    • ForkJoinPool
    • CyclicBarrier CountDownLatch
  • 其他
    • threadLocal
    • 多线程开发好习惯
  • 重排序、原子性、可见性
    • 重排序
      • as-if-serial
    • 原子性
    • 可见性
      • volatile
      • synchronized

基本概念

基本概念解释

  • 进程:
    • 程序运行的最基本的单位,一个程序最起码一个进程
    • 一个进程有独立的内存单元。
    • 创建进程的代价很高
    • 一个进程的崩溃不会影响其他进程
    • 多进程程序可以跨机器部署
  • 线程:
    • 多个线程共享一个进程内存单元
    • cpu调度是按线程调度
    • 一个进程至少一个线程
    • 创建线程的开销很小,线程本身占有的资源很少
    • 一个线程死掉后整个进程挂掉
    • 多个线程只能运行在一个机器上

https://www.cnblogs.com/fuchongjundream/p/3829508.html

  • 守护线程:jvm不会等待守护线程,非守护线程全部退出后,jvm退出。守护线程结束。eg:gc线程
  • 非守护线程:jvm会等待所有的非守护线程运行结束
  • 多线程上下切换:cpu的控制权从一个正在执行的线程切换到一个就绪等待的线程
  • 线程调度算法:抢占式。cpu会根据线程优先级、饥饿度为线程排序,给一个线程一个时间片段执行
  • Thread.sleep(0)手动触发系统分配时间片操作

线程的状态

  • 新建-》就绪:new Thread().start()
  • 就绪-》运行:获取到处理器资源
  • 运行-》死亡:任务执行完成、运行报错、Thread.stop()
  • 运行-》阻塞:Thread.sleep()、io阻塞、等待锁、等待唤醒、suspend()
  • 阻塞-》就绪:sleep结束、io返回、获得锁、唤醒、resume()
  • 运行-》就绪:yield()、失去到处理器资源

线程

创建

  • 继承Thread,使用start()启动,一个Thread实例只能启动一次
  • 实现Runnable,放入Thread执行
    • 由于java是单继承,所以使用实现Runable的方式更好,相比继承Thread
    • 可以多个线程同时执行一个实例,数据共享
  • 实现Callable,放入FutureTask执行
    • 相比Runnable。1、Callable中的call()是有返回值的,通过和Future配合可以取到执行结果。2、可以抛出异常。3、Callable实现call(),Runnable实现run()。4、可以通过future获取Callable的运行状态,取消任务
FutureTask<String> future = new FutureTask<>(new Callable<String>() {
    public String call() throws Exception {
        return "";
    }
});
new Thread(future).start();
  • 使用线程池

FutureTask

- FutureTask(Callable)
	- RunnableFuture
		- Runnable
		- Future
  • 实现Runnable Future接口。Runnable使得可以放入到Thread中,Future使得可以使用get()来获取返回值
  • Runnable、Callable作为FutureTask构造参数传入
  • 最终FutureTask放入Thread中启动线程.
  • 可以放入线程池
  • FutureTask相比于Runable特殊的功能
    • 内部维护着任务的运行状态
    • 内部存储着返回值,可以在任何时刻获取
    • 可以有返回值、抛异常
    • 一个FutureTask只能运行一次(因为里面维护了运行状态),如果需要多次运行,需要创建多个FutureTask然后传入同一个Callable

调用Thread中的start() 和run()

  • start():启动一个新线程运行run()中的逻辑
  • run():在当前线程运行run()

Thread.holdsLock(obj)

  • 检查当前线程是否持有obj监视器

线程如果遇到异常

  • 如果没有捕获,线程停止
  • 线程释放获取的锁

线程同步

  • 多个线程访问同一个数据时,容易偶然性的出现线程安全的问题

synchronized

  • synchronized关键字是java内建的同步机制,也是java5前仅有的方式
  • 通过该关键字可以制造同步方法和同步代码块
  • 如果一个线程获取到锁,其他线程只能阻塞等待

suspend() resume() stop()(废弃)

  • 持有锁时suspend,线程进入阻塞状态但并不释放所占用的锁
  • suspend所以如果使用不当会造成对公共对象的独占,使得其他线程无法访问公共对象,严重的话造成死锁
thread.suspend();

参考:https://www.cnblogs.com/AK47Sonic/p/7672606.html

  • stop() 终止一个线程时会强制中断线程的执行,不管run方法是否执行完了,并且还会释放这个线程所持有的所有的锁对象
  • 这样可能会造成数据的不一致

https://blog.youkuaiyun.com/xingjiarong/article/details/47984659

wait() notify() notifyAll()

  • 这三个方法都必须在获取到锁的情况下才能调用,所以放在synchronized中
  • wait()会立刻释放对象监视器;notify()和notifyAll()会在剩余代码执行完后释放
  • 为什么不放在Thread上:java的锁时对象级的锁,每个对象都是锁。如果定义在Thread上就不能清楚反应释放获取哪个锁
  • wait()和while 还是 if配合:与while配合,唤醒后应该在检查一下条件,如果不满足再次wait。
  synchronized (monitor) {
        //  判断条件谓词是否得到满足
        while(!locked) {
            //  等待唤醒
            monitor.wait();
        }
        //  处理其他的业务逻辑
    }

wait()和while:https://blog.youkuaiyun.com/yiifaa/article/details/76341707

sleep() 和wait()

  • sleep()不会释放对象锁,wait()会
  • sleep()睡眠后不会让出cpu,wait()会

ReentrantLock

  • 再入锁,在java5中提供
  • 通过lock()方法获取锁
  • 显示的加锁解锁
  • 能够实现比synchronized更加细化的锁控制
  • 和synchronized相比,在早起版本有较大的优势,但是在后续版本中,低竞争环境下性能不如synchronized
    • lockInterruptibly():提供了中断响应,可以再必要的时候interrupt(),来解决死锁问题
    • tryLock():申请锁,等待一段时间,如果没有成功获取到锁,则返回false
    • new ReentrantLock(true):创建公平锁,所有获得锁的请求都排队等待,按顺序获取锁
    • Condition cond=lock.newCondition();condition上提供了await() signal() signalAll()。可以更加精细的控制锁的唤醒

参考:https://blog.youkuaiyun.com/Somhu/article/details/78874634

sychronized和ReentrantLock

  • sychronized是关键字,ReentrantLock是类
  • ReentrantLock可以灵活的实现多路通知、公平锁、tryLock()这种方法

死锁

  • 多个线程竞争统一资源
  • 获取到资源的线程不释放资源
  • 等待资源的线程只能堵塞
public class DeadLock {
    private static ReentrantLock lock1=new ReentrantLock();
    private static ReentrantLock lock2=new ReentrantLock();
    private static class Task implements Runnable{
        private boolean flag=false;
        public Task(boolean flag){
            this.flag=flag;
        }
        @Override
        public void run() {
            try {
                if (this.flag) {
                    lock1.lock();
                    Thread.sleep(100);//休眠100ms增加死锁的概率
                    lock2.lock();
                } else {
                    lock2.lock();
                    Thread.sleep(100);
                    lock1.lock();
                }
            }catch (Exception e){
            }finally {
                if(lock1.isHeldByCurrentThread())lock1.unlock();
                if(lock2.isHeldByCurrentThread())lock2.unlock();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Task task1=new Task(false);
        Task task2=new Task(true);
        Thread t1=new Thread(task1);
        Thread t2=new Thread(task2);
        t1.start();
        t2.start();
    }
}

偏斜锁、自旋锁、轻量级锁、重量级锁?

  • 自旋锁:1.6后默认开启,基于之前的观察,锁定状态只会持续很短的时间,线程挂起恢复很浪费,所以就不放弃cpu执行权,让线程执行一个忙循环
  • 偏向锁:锁会倾向于第一个获取它的线程,如果后面该锁没有被其他线程获取,则持有偏向锁的线程将不用进行同步

锁膨胀、降级

乐观锁和悲观锁

  • 悲观锁:悲观的认为竞争会发生,所有操作先上锁。eg:synchronized
  • 乐观锁:乐观的认为不会有竞争,将比较替换作为一个原子操作,如果失败表示有冲突,则重试
  • CAS(Compare And Swap)是一种常见的“乐观锁”,大部分的CPU都有对应的汇编指令,它有三个操作数:内存地址V,旧值A,新值B。只有当前内存地址V上的值是A,B才会被写到V上,否则操作失败。

乐观锁:https://blog.youkuaiyun.com/m0_37585523/article/details/71107867
cas机制:https://www.cnblogs.com/mmmmar/p/8624242.html

线程间共享数据

  • synchronized和wait() notify() notifyAll()
  • ReentrantLock和await() signal() signalAll()
  • BlockingQueen;
  • 典型的生产者消费者模式
  • 通过平衡生产者消费者提高运行效率
  • 使生产者和消费者间解耦

线程池

Executor ExecutorService接口

- ForkJoinPool
- ThreadPoolExecutor
    - AbstractExecutorService
        - ExecutorService: submit()返回一个future,包含返回结果、异常,既可以传入Runnable也可以使Callable。
            - Executor: execute()只能传入Runnable,不会有异常和返回值。

https://www.cnblogs.com/wanqieddy/p/3853863.html

线程池

  • 线程池可以灵活控制并发数,减少创建销毁线程的开销
  • 核心类:ThreadPoolExecutor。继承ExecutorService
  • java提供了简化的创建线程池的工厂Executors
    • newFixedThreadPool():线程数固定,队列为Integer最大值长度
    • newSingleThreadPool():线程数只有一个,队列为Integer最大长度
    • newCachedThreadPool():有新任务来将会使用空闲线程或者新建一个线程来处理,由于队列中不缓存任务,所以可能创建很多线程导致资源耗尽。每个空闲线程只能存活60秒
    • newScheduledThreadPool():创建一个定时调度的线程池
    • newSingleThreadScheduledExecutor():创建一个单线程的定时调度线程池
    • 不推荐使用Executors来创建线程池:https://blog.youkuaiyun.com/admin1973/article/details/78685795
  • LinkedBlockingQueue用来装计划执行的任务
  • ThreadPoolExecutor
    • execute():向线程池提交任务
    • submit():向线程池提交任务,能返回执行结果(Callable)
    • shutdown()是用来关闭线程池的
    • shutdownNow()是用来关闭线程池的,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务
    • corePoolSize就是线程池大小
    • maximumPoolSize在我看来是线程池的一种补救措施,即任务量突然过大时的一种补救措施
    • largestPoolSize只是一个用来起记录作用的变量,用来记录线程池中曾经有过的最大线程数目,跟线程池的容量没有任何关系
  • 任务放入线程池的过程:
    • 如果线程数 < corePoolSize,直接创建一个线程执行
    • 如果线程数 > corePoolSize,将任务放入到任务队列
    • 如果线程数 > corePoolSize,任务队列也放满了,则创建一个新线程执行
    • 如果线程数 > corePoolSize,任务队列也放满了,线程数 > maximumPoolSize,执行丢弃策略
  • 如果用LinkedBlockingQueue:默认是无限大的任务队列
  • 如果用ArrayBlockingQueue:则必须设置大小,超过后先创建线程到最大上限,如果还有则执行丢弃策略
  • 丢弃策略:
    • AbortPolicy:丢弃并抛异常
    • CallerRunsPolicy:在提交线程中运行
    • DiscardOldestPolicy:将最久的任务丢掉,随后将此任务插入到队列中
    • DiscardPolicy:丢弃

ThreadPoolExecutor:https://www.cnblogs.com/dolphin0520/p/3932921.html
newCachedThreadPool:https://www.cnblogs.com/baizhanshi/p/5469948.html
丢弃策略:https://blog.youkuaiyun.com/jgteng/article/details/54411423

concurrent

java.util.concurrent.atomic

  • 包下的AtomicInteger、AtomicReference等类,它们提供了基于CAS的读写操作和并发环境下的内存可见性。
  • 使用atomic类同时解决可见性问题(内部存储使用了volatile),和操作原子性问题(通过cas非阻塞算法实现)
public class AtomicDemo {
//   private static int num=0;
    private static AtomicInteger atomicInteger=new AtomicInteger(0);
    private static int threadNum=10000;
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads=new Thread[threadNum];
        for (int i=0;i<threadNum;i++){
            threads[i]=new Thread(()->{
                try {
                    Thread.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
//                num++;
                atomicInteger.incrementAndGet();
            });
            threads[i].start();
        }
        for(int i=0;i<threadNum;i++){
            threads[i].join();
        }
//        System.out.println(num);
        System.out.println(atomicInteger.get());
    }
}

AtomicInteger: https://blog.youkuaiyun.com/fanrenxiang/article/details/80623884

concurrentHashMap

  • 1.5中提供,相比hashtable性能更高,1.6中concurrentHashMap采用的是局部上锁解决多线程安全问题
  • 1.8中concurrentHashMap采用了cas实现同步,同时在数组+链表的基础上在添加了红黑树

fork/join

- RecursiveTask
- RecursiveAction
    - ForkJoinTask: 
    				fork():拆分任务,放入线程池,准备执行
                     join():返回结果进行合并

- ForkJoinPool:invoke(ForkJoinTask<T> task) 提交任务

参考:https://www.cnblogs.com/wzqjy/p/7921063.html

CyclicBarrier CountDownLatch

  • CyclicBarrier:同步屏障,等待指定数量的线程await()后,将触发被await的线程重新运行。可以reset()反复执行。还有其他的方法,比如获取被阻塞的线程数等

参考:http://ifeve.com/concurrency-cyclicbarrier/

  • CountDownLatch :作用和CyclicBarrier一样,但是只能执行一次
  • 主要的使用场景如下:
    • 让一个线程1 await(),其他线程运行结束后countDown(),当所有线程结束后线程1将重新开始运行
    • new CountDownLatch(1) ,所有线程被同一个CountDownLatch实例await();主线程一旦countDown(),所有线程同时开始运行

https://blog.youkuaiyun.com/joenqc/article/details/76794356

其他

threadLocal

  • 仅仅局限在线程内部的变量,属于线程自身,不在多个线程间共享
  • 一种用空间换时间的做法,将每个线程的数据隔离开,没有线程安全问题,自然不用同步相关操作

多线程开发好习惯

  • 最小同步范围
  • 优先使用volatile
  • 考虑使用线程池
  • 优先使用并发容器

重排序、原子性、可见性

重排序

  • 代码书写的顺序和实际执行顺序不同,主要目的是为了编译器和处理器提高运行效率
int a=0;
int b=9;
//实际的执行顺序可能是:
int b=9;
int a=0;

as-if-serial

  • 无论怎么重排序,程序的执行结果和代码顺序执行的结果一样
  • java会保证在单线程的情况下遵循as-if-serial
int a=0;
int b=a+4;//这两行不能进行重排序

原子性

  • 保证程序要么一起执行,要么都不执行,执行过程中不会被其他的线程影响
  • 保证原子性可以使用:
    • synchronized
    • ReentrantLock
    • AtomicInteger

可见性

  • 共享变量在线程间的可见性:一个线程对共享变量的修改能及时被其他的线程看到
  • 共享变量:如果一个变量在多个工作线程中使用、在工作内存中存在副本,那么这个变量就是这几个线程的共享变量
  • java内存模型:
    • 所有变量都存储在主内存中
    • 每个线程都有一个独立的工作内存,里面保存着该线程使用到的变量副本
    • 线程对共享变量的所有操作都必须在自己的内存中进行,不能直接读取主内存
    • 每个线程的工作内存相互独立
  • 可见性:线程一将工作内存1中的数据刷新到主内存,线程二将主内存中的值更新到工作内存2中
  • 导致不可见的原因:
    • 线程的交叉执行
    • 重排序同时线程交叉执行
    • 共享变量没及时在主内存和共享内存更新
  • 可见性问题只有在高并发下才会出现(主要是内存来不及刷新)

synchronized实现可见性

  • 在获取到锁后先从主内存获取最新的值
  • 在解锁前先将工作内存中的值刷新到主内存中
  • 保证原子性

volatile实现可见性

  • 通过内存屏蔽和禁止重排序来实现可见性
  • 写:在工作线程被更新后,会及时刷新到主线程。底层通过在后面插入一条store命令实现
  • 读:先从主内存中获取值更新到工作线程中。底层通过在面前插入一条load命令实现
  • volatile不能保证原子性(eg:a++)
  • volatile一般不能保证原子性,所以想要在多线程安全使用,则需要:
    • 对变量的写入不依赖当前值(a++)
    • 该变量没有包含在具有其他变量的不变式中(a>b)
  • 读取long、double64位数据的时候保证原子性

final实现可见性

  • 因为final一旦赋值就不能修改,所以在线程间是可见的

volatile和synchronized

  • volatile更轻量级
  • volatile不能保证原子性

64位(long,double)数据操作不是原子性

  • jvm分两次读取内存,不是原子性的
  • 可以用volatile解决

对数组使用volatile

  • 如果改变数组索引,将受到volatile的保护,如果改变数组中的元素,volatile起不到保护作用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值