线程知识总结

2022-03-05  本文已影响0人  三十五岁养老

一、线程

线程就是进程中运行的多个子任务,是操作系统调用的最小单元

线程的状态

  1. 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
  2. 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
  3. 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。

线程状态之间的切换

线程状态切换.png

几个方法的比较

二、线程池

线程池的工作原理:线程池可以减少创建和销毁线程的次数,从而减少系统资源的消耗
当一个任务提交到线程池时:
a. 首先判断核心线程池中的线程是否已经满了,如果没满,则创建一个核心线程执行任务,否则进入下一步
b. 判断工作队列是否已满,没有满则加入工作队列,否则执行下一步
c. 判断线程数是否达到了最大值,如果不是,则创建非核心线程执行任务,否则执行饱和策略,默认抛出异常

线程池的种类

  1. FixedThreadPool:可重用固定线程数的线程池,只有核心线程,没有非核心线程,核心线程不会被回收,有任务时,有空闲的核心线程就用核心线程执行,没有则加入队列排队
  2. SingleThreadExecutor:单线程线程池,只有一个核心线程,没有非核心线程,当任务到达时,如果没有运行线程,则创建一个线程执行,如果正在运行则加入队列等待,可以保证所有任务在一个线程中按照顺序执行,和FixedThreadPool的区别只有数量
  3. CachedThreadPool:按需创建的线程池,没有核心线程,非核心线程有Integer.MAX_VALUE个,每次提交
    任务如果有空闲线程则由空闲线程执行,没有空闲线程则创建新的线程执行,适用于大量的需要立即处理的并且耗时较短的任务
  4. ScheduledThreadPoolExecutor:继承自ThreadPoolExecutor,用于延时执行任务或定期执行任务,核心线程数固定,线程总数为Integer.MAX_VALUE

三、线程安全

Java并发的问题要从JMM讲起,

JMM
  1. 在Java内存模型中,分为主内存和线程工作内存,线程使用共享数据时,都是先从主内存中拷贝到工作内存,使用完成之后再写入主内存。
  2. 每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的值的传递均需要通过主内存来完成。可以理解为线程之间通讯是通过共享内存的方式实现的。
  3. 在多线程环境下,不同线程对同一份数据操作,就可能会产生不同线程中数据状态不一致的情况,这就是线程安全问题的定义或者原因。
    线程安全需要保证数据操作的两个特性:

四、锁

synchronized:JVM层面实现的,系统监控锁的释放与否;在并发量比较小的情况下,使用synchronized是个不错的选择。可以与wait()、notify()、nitifyAll()一起使用,从而进一步实现线程的通信。
ReentrantLock:使用代码实现的,系统无法自动释放锁,需要在代码中的finally子句中显式释放锁lock.unlock()。并发量比较高的情况下,使用ReentrantLock。


image.png

降低锁的竞争程度

串行操作会降低可伸缩性,上下文切换会减低性能

CAS算法

CAS是英文单词Compare and Swap(比较并交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数
1.需要读写的内存值 V
2.进行比较的值 A
3.拟写入的新值 B
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B,否则不会执行任何操作。一般情况下是一个自旋操作,即不断的重试。

自旋锁实现

当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
本身无法保证公平性,同时也无法保证可重入性,基于自旋锁,可以实现具备公平性和可重入性质的锁(引入计数器)。


public class SpinLock {
   private AtomicReference<Thread> cas = new AtomicReference<Thread>();
   public void lock() {
       Thread current = Thread.currentThread();
       // 利用CAS
       while (!cas.compareAndSet(null, current)) {
           // DO nothing
       }
   }
   public void unlock() {
       Thread current = Thread.currentThread();
       cas.compareAndSet(current, null);
   }
}

lock()方法利用的CAS,当第一个线程A获取锁的时候,能够成功获取到,不会进入while循环,如果此时线程A没有释放锁,另一个线程B又来获取锁,此时由于不满足CAS,所以就会进入while循环,不断判断是否满足CAS,直到A线程调用unlock方法释放了该锁。

自旋锁存在的问题

自旋锁的优点

五、线程安全数据结构

image.png
  1. HashTable
    HashTable实现了Map接口,为此其本身也是一个散列表,它存储的内容是基于key-value的键值对映射。
    HashTable使用synchronized来修饰方法函数来保证线程安全,但是在多线程运行环境下效率表现非常低下。因为当一个线程访问HashTable的同步方法时,其他线程也访问同步方法就会粗线阻塞状态。比如当一个线程在添加数据时候,另外一个线程即使执行获取其他数据的操作也必须被阻塞,大大降低了程序的运行效率。

  2. ConcurrentHashMap
    ConcurrentHashMap是HashMap的线程安全版。但是与HashTable相比,ConcurrentHashMap不仅保证了多线程运行环境下的数据访问安全性,而且性能上有长足的提升。
    ConcurrentHashMap允许多个修改操作并发运行,其原因在于使用了锁分段技术:首先讲Map存放的数据分成一段一段的存储方式,然后给每一段数据分配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。这样就保证了每一把锁只是用于锁住一部分数据,那么当多线程访问Map里的不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问效率。
    上述的处理机制明显区别于HashTable是给整体数据分配了一把锁的处理方法。为此,在多线程环境下,常用ConcurrentHashMap在需要保证数据安全的场景中去替换HashMap,而不会去使用HashTable。

  3. CopyOnWriteArrayList
    CopyOnWriteArrayList实现了List接口,提供的数据更新操作都使用了ReentrantLock的lock()方法来加锁,unlock()方法来解锁。
    当增加元素的时候,首先使用Arrays.copyOf()来拷贝形成新的副本,在副本上增加元素,然后改变原引用指向副本。读操作不需要加锁,而写操作类实现中对其进行了加锁。因此,CopyOnWriteArrayList类是一个线程安全的List接口的实现,在高并发的情况下,可以提供高性能的并发读取,并且保证读取的内容一定是正确的,这对于读操作远远多于写操作的应用非常适合

  4. CopyOnWriteArraySet
    CopyOnWriteArraySet是对CopyOnWriteArrayList使用了装饰模式后的具体实现。所以CopyOnWriteArrayList的实现机理适用于CopyOnWriteArraySet。

  5. ConcurrentLinkedQueue
    可以被看作是一个线程安全的LinkedList,使用了非阻塞算法实现的一个高效、线程安全的并发队列;其本质是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当添加一个元素时会添加到队列的尾部;当获取一个元素时,会返回队列头部的元素。

  6. LinkedBlockingQueue
    线程安全的,实现了先进先出等特性,是作为生产者消费者的首选,内部则是基于锁,并提供了BlockingQueue的等待性方法。

  7. Vector
    Vector通过数组保存数据,继承了Abstract,实现了List;所以,其本质上是一个队列
    但是和ArrayList不同,Vector中的操作是线程安全的,它是利用synchronized同步锁机制进行实现,其实现方式与HashTable类似。

  8. StringBuffer与StringBuilder
    对于频繁的字符串拼接操作,是不推荐采用效率低下的“+”操作的。一般是采用StringBuffer与StringBuilder来实现上述功能。但是,这两者也是有区别的:前者线程安全,后者不是线程安全的。
    StringBuffer是通过对方法函数进行synchronized修饰实现其线程安全特性,实现方式与HashTable、Vector类似。

————————————————
参考链接:https://blog.csdn.net/qq_29229567/article/details/87799838

六、线程常见问题

1、消费者生产者模式实现

  1. 使用LinkedBlockingQueue
    LinkedBlockingQueue实现是线程安全的,实现了先进先出等特性,是作为生产者消费者的首选,LinkedBlockingQueue 可以指定容量,也可以不指定,不指定的话,默认最大是Integer.MAX_VALUE,其中主要用到put和take方法,put方法在队列满的时候会阻塞直到有队列成员被消费,take方法在队列空的时候会阻塞,直到有队列成员被放进来。
public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
            while (count.get() == 0) { //队列大小
                notEmpty.await(); //释放CPU资源,进入等待,被唤醒后 继续while循环,不为空则跳出循环
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal(); //唤醒一个等待在Condition(等待队列)上的线程,将该线程由等待队列转移到同步队列
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }

  1. lock和condition的await、signalAll
  2. synchronized、wait和notify
    参考链接: https://www.cnblogs.com/fankongkong/p/7339848.html

2、Thread为什么不能用stop方法停止线程

  1. 即刻抛出ThreadDeath异常,在线程的run()方法内,任何一点都有可能抛出ThreadDeath Error,包括在catch或finally语句中。
  2. 释放该线程所持有的所有的锁。调用thread.stop()后导致了该线程所持有的所有锁的突然释放,那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。

3、如何解决哲学家就餐问题

参考:https://blog.csdn.net/qq_16546235/article/details/109612177

4、为什么说String是线程安全的

String是final修饰的类,代表了String的不可继承性


image.png

final修饰的char[]代表了被存储的数据不可更改性。但仅仅是引用地址不可变,并不代表了数组本身不会变,起作用的还有private,正是因为两者保证了String的不可变性。

Java String类为什么是final的?

Java final的用途?

5、为什么HashMap线程不安全

6、Java 中 Lock 接口比 synchronized 块的优势是什么?

Lock接口在多线程编程中最大的优势是它们分别为读和写提供了锁。

7、如何实现一个高效的缓存,它允许多个用户读,但只允许一个用户写?

使用读写锁ReentrantReadWriteLock,多个线程可以同时进行读取操作,但是同一时刻只允许一个线程进行写入操作。

上一篇下一篇

猜你喜欢

热点阅读