JavaSE基础知识梳理——多线程并发

10.1 基本概念

  • 进程之间内存不共享

  • Java中线程间堆内存和方法区共享,栈内存独立(很重要),方法区只有1块,堆也只有1块,栈可能有多个,主线程对应主栈,在进程中创建的变量或对象的引用位于栈中,不共享。(多个进程间不共享局部变量)

  • 每个栈与每个栈之间之间互不干扰,各自执行,这就是多线程并发。

  • 堆,栈,方法区

  • image-20200606002653497.png
  • 线程的生命周期:

    NEW:创建

    RUNNABLE:启动线程之后,被唤醒之后,有被CPU调度的机会

    WAITING:进入等待集

    WAITINGTIME:带阻塞时间的等待

    RUNNING:运行态

    BLOCKING:阻塞态

    TERMINATED:死亡态

  • 线程的运行由CPU调度,每一次运行结束的位置由程序计数器计算,下一次从上一次断开的位置继续。

10.2 创建线程的方法

  • 创建线程的方法有很多种:
  1. extends Thread,重写run()方法
MyThread t = new MyThread()
  1. implements Runnable,重写run()方法。==Runnable只是一个普通接口==
Runnable r = new MyRunnable();
Thread t = new Thread(r);
  1. implements Callable,重写call方法。==Callable也只是一个普通接口==
Callable c = new MyCallable();
//用FutureTask去接收Callable,FutureTask是一个有返回值的线程,因为call()方法有返回值
FutureTask f = new FutureTask(c);
f.run();
f.get();//get是一个阻塞性方法,会一直获取。也可以指定等待时间
  1. 传入一个线程Thread1,==Thread类实现了Runnable接口==
Thread t = new Thread(Thread1)
  1. 传入 FutureTask,==FutureTask实现了RunnableFuture接口,RunnableFuture继承至Runnable==
Thread t = new Thread(new FutureTask(new Callable(){重写call()方法}))
  1. 线程池ThreadPoolExecutor,在后面介绍
  2. 线程组ThreadGroup

10.3 线程的常用方法

  • 开启线程

    //Thread类
    void start();
    //FutureTask类
    Object run();
    

    注意如果直接调用run()方法不会开启线程。

  • 设置守护线程

//守护线程会等待到所有用户线程结束后自动结束
void setDeamon(true);
  • 中断,设置中断标志
/*设置中断标志并不会中断线程,如果处于wait,sleep,join等阻塞态被中断时,会抛出InterruptedException异常
。在catch到异常之后,中断标志重置为false*/
void interrupt();
Thread t ;
t.interrupt();
  • 检测中断的两个方法
//静态方法
Thread.interrupted();//检测当前线程中断标志位,如果为true,则返回true并重置为false。
//实例方法
void isInterrupted();//检测中断标志,不重置
  • 阻塞线程的方法
1.void sleep(long timeMills);//挂起线程持续timeMills时间,单位毫秒
2.void wait()
  void wait(long timeMills) //需要先获取锁,synchronized,然后用该锁执行wait(),线程将自己挂起并释放锁
3.void join() //将调用者加到当前线程,等待调用者执行完毕。
  • 唤醒线程,使线程重新回到Runnable态
//唤醒线程也要先获取锁,synchronized,并且用和wait相同的锁去notify
void notify()//随机唤醒单个线程
void notifyAll()//唤醒所有在等待中的线程,只会唤醒在调用notifyAll之前就处于等待的线程。

==notify造成死锁的原因分析==

​ 假如有消费者C1和C2,生产者P1,C1先拿到锁,发现没有产品,就wait,然后释放锁,锁被P1拿到了,就产品+1,然后notify,然后下一次争抢锁,P1抢到了,发现有产品,就wait,然后释放锁,锁被C1或者C2拿到了,消费,然后notify,释放锁,再一次抢夺锁时,只有消费者抢到了,则它就会wait,这把锁只要被消费者抢到,因为没有产品,就只能wait。就进入了死锁。即只要生产者连续两次拿到锁,必然会生产产品,然后第二次将自己挂起,后面只要消费者消费了之后唤醒的不是生产者,就会使得所有消费者都获得锁,然后把自己挂起。最终进入死锁。

所以结论是,所有生产者和消费者用notifyAll一定不会死锁,因为这样会唤醒所有挂起的线程,不存在忽略了生产者而造成后续的死锁。死锁产生的根本原因是生产者只唤醒生产者,消费者只唤醒消费者,最终生产者都认为满了,然后挂起;消费者消费完了,唤醒别的消费者,别的消费者就会挂起,直到最先的消费者被挂起。而如果都是用notifyAll,就不分差异的唤醒所有

  • 让出CPU执行权
void yield();//让出当前的CPU执行权,让CPU重新调度,有可能再次被调度到
  • 设置线程优先级
void setPriority();//默认等级5,最高10,最低1。优先级越高,被调度到的概率越大

10.4 线程同步

  • 线程同步是解决线程安全问题的方法,主要有==synchronized==同步,==Lock==同步和==CAS非阻塞算法==同步。CPU效率依次递增

1、synchronized关键字

  • synchronized作用于方法内

    //synchronized(lock),lock一般是共享的资源,虽然用其他唯一的实例也可以,但是推荐用共享资源
    @override
    public void run(){
        synchronized(lock){
            //do something
        }
    }
    
    • synchronized作用于方法上

      1.实例方法
      public synchronized void method(){} //此时的锁为this,实例对象
      
      2.静态方法
      public synchronized static void method(){} //此时的锁为类锁,class,它是唯一的
      

2、volatile关键字

  • volatile只保存内存可见性,它不保证线程安全
  • volatile修饰的变量在发生变化时,会及时通知其它线程,重新从主内存中取值到工作内存。但是因为volatile不是原子性操作,因此存在线程安全。
  • image-20200606114713038.png
  • 总线嗅探机制:线程A修改信息后在汇编指令中会加Lock前缀,修改信息通过总线被线程B嗅探到,(因为cpu和内存的交互通过I\O总线),此时B就把原来内存空间的变量变为无效状态。重新从主内存中读取。
  • volatile不保证线程安全的原因:

因为线程A在引擎从工作内存中拿到值之后,读,改是分步的,这时候如果另外一个线程B已经完成了写到主内存的操作,假如写入的值为2。这时候线程A会通过嗅探机制,感知到volatile修改的值的变化,会将自己工作内存的变量舍去,置位无效,但是引擎已经读取了值了,并且完成了值的修改,假如修改为了1。这时候,引擎就会assgin,把值存到工作内存,然后再写到主内存,就会将原来主内存的值给覆盖了。

3、Lock锁

  • java.util.concurrent.locks包下的Lock是一个接口,其常用实现类为ReentrantLock,效率比synchronized高,更安全
  • Lock锁也只有释放了才能被其它线程获取到。
  • ==ReentrantLock==
//它是一个独占锁
new ReentrantLock(boolean fair);//fair表示是否公平
    公平:先到先得
    不公平:先到不一定先得,随机,默认是不公平

常用方法:

//ReentrantLock类
void lock();//上锁
void unlock();//释放锁

boolean tryLock(long time,TimeUnit timeUnit);//尝试获取锁,成功则返回true,失败则在规定的时间内多次尝试
Condition newCondition();//获取Condition对象,这个对象有await()和signal,signallAll方法,相当于wait和notify,notifyAll

//Condition类
void await();//相当于wait()
void await(int timeMills);//相当于wait(int timeMills)
void signal();
void signalAll();

示例:

while (true){
    if (lock.tryLock(2, TimeUnit.SECONDS)){
            lock.lock;
        try {
            System.out.println("我获取到锁了");
        }finally {
            lock.unlock();
            break;
        }
    }
}
class TaskQueue {
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    private Queue queue = new LinkedList<>();


    public void addTask(String s) {
        lock.lock();
        try {
            queue.add(s);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }


    public String getTask() {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                condition.await();
            }
            return queue.remove();
        } finally {
            lock.unlock();
        }
    }
}
  • ==ReentrantReadWriteLock==

    //读写锁,写的优先级高于读。
    读写锁,多个线程可以同时获得读锁,但写锁是独占的,这样的好处是提高并发效率。适用于多个线程读,少数线程写。且没有写的情况下,才能读。这是一种悲观锁。
    读锁和写锁本质上是同一个锁,只是读锁可以共享。所以在读的过程中也要加锁,防止被写占有,导致读的过程中数据变更引起脏读
    //示例
    public class Counter {
        private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
        private final Lock rlock = rwlock.readLock();
        private final Lock wlock = rwlock.writeLock();
        private int[] counts = new int[10];
    
    
        public void inc(int index) {
            wlock.lock(); // 加写锁
            try {
                counts[index] += 1;
            } finally {
                wlock.unlock(); // 释放写锁
            }
        }
    
    
        public int[] get() {
            rlock.lock(); // 加读锁
            try {
                return Arrays.copyOf(counts, counts.length);
            } finally {
                rlock.unlock(); // 释放读锁
            }
        }
    }
    
    • ==StampledLock==

    这是一种乐观锁,允许在读的时候写,性能比ReentrantReadWriteLock高


4、JUC包下的原子类

  • Atomic类采用的是CAS非阻塞算法,使用的是Unsafe类的compareAndSwap()作为基础。Unsafe类下还有很多其它方法,参考==《并发编程之美》==。

  • CAS是机器级别的指令,效率比加锁的方式高。

  • 并发包分类:

    1. 其中LongBinaryOperator中的left就是每次accumulate运算后的结果,第一次计算时是构造LongAccumulator时传入的identity,right是accumulate传入的参数。
    2. LongAdder和LongAccumulator是相对于Atomic类而言,它们类似ThreadLocal,每个线程都有一个自己的复制变量,不需要再为了竞争原子资源而消耗时间。
    3. 原子类和锁的不同之处在于锁是阻塞的,只能被一个线程只有,其它线程都必须阻塞等待。而原子类不会阻塞线程,只不过同一时间只有一个线程可以操作成功。
image-20200606121302916.png

10.5 ThreadLocal和ThreadLocalRandom

1、ThreadLocal类

  • ThreadLocal是本地线程变量,它是一个泛型类,可以存储任意类型。每个线程都有一个ThreadLocal的复制。互不影响。==但是必须先在自己的虚拟机栈里set这个值==。一般定义为==static==,最后要在==finally里清除ThreadLocal==。
  • 常用方法
void set();
Object get();
void remove()清除

2、ThreadLocalRandom类

  • ThreadLocalRandom是多线程下使用的Random随机数类。
ThreadLocalRandom.current()//获取当前线程的ThreadLocalRandom对象。

10.6 线程池

1、基本概念

  • 线程池和线程组:

    在main方法中自定义的线程都属于同一个线程组,同一个线程组共享变量,但是线程组使用较少。

    线程池的效率高,它降低了创建线程的开销。

  • 线程池的关系

==主要接口视图关系==:

Executor:线程池的顶级接口

ExecutorService:线程池的真正接口

ScheduledExecutorService:需要执行重复任务的线程池的接口

ThreadPoolExecutor:ExecutorService的默认实现类

ScheduledThreadPooleExecutor:继承至ThreadPoolExecturo,实现ScheduledExecutorService接口

如果要执行非重复任务,就转为ExecutorServiece,如果要执行重复任务,就转为ScheduledExecutorService。

image-20200606124629735.png
  • 工具类

==Executors==提供一些静态工厂,用于创建不同类型的线程池。

  • 线程池的一些参数

    ==corePoolSize==:核心线程数量
    ==maximumPooleSize==:最大线程数量

    ==keepAliveTime==:线程没有任务时,最多保存多长时间后会终止

2、线程池分类

  • ==newCachedThreadPool==
//创建一个带有缓冲的线程池,缓冲的意思是可以动态调整线程池的核心线程数量,当超过了默认大小时,就扩容。可以人为指定一个区间。
ExecutorService service = Executors.newCachedThreadPool(); //线程池,无大小限制
  • ==newFixedThreadPool==
//创建一个指定大小的线程池
ExecutorService service = Executors.newFixedThreadPool(2);
  • ==newSingleThreadPoolExecutor==
//创建一个单线程的线程池,线程是串行执行任务
ExecutorService service = Executors.newSingleThreadExecutor()
//单线程的执行顺序,先执行t1再t2,再t3,相当于固定大小为1
service.execute(t1);
service.execute(t2);
service.execute(t3);
  • ==newScheduledThreadPool==

    //周期性的执行任务
    ExecutorService service = Executors.newScheduledThreadPool();
    service.schedule(t1,2, TimeUnit.SECONDS);//延迟多久执行,只执行一次,一次性任务
    service.scheduleAtFixedRate(t1,1,2,TimeUnit.SECONDS);//任务,第一次执行的时间,间隔多久执行一次,时间单位
    service.scheduleWithFixedDelay(t1,1,2,TimeUnit.SECONDS);//任务,第一次执行的时间,间隔多久执行一次,时间单位
    service.shutdown();//关闭线程池
    
    scheduleAtFixedRate 间隔多久就执行一次,从任务一开始就计时,不管上一次任务是否执行完毕。但是如果该任务的执行时间大于周期,那么后续任务可能会延迟开始,但是不会并发执行
    scheduleWithFixedDelay 间隔多久执行一次,指的是等上一次任务执行完毕,再间隔多久,执行
    • 线程池的执行任务方法:

      //submit
      Object submit(Callable callable);//适用于有返回值的线程,虽然也可以接收Runnable,这是返回的是null,但是为了区分,最好接收Callable。
      
      //execute
      void execute(Runnable runnable);//执行无返回值的线程
      

你可能感兴趣的:(JavaSE基础知识梳理——多线程并发)