全国旅游景点网站开源,平台运营需要多少钱,一条视频可以多平台发布吗,淘宝店有给网站做优化am多线程 计算机在同一时间可以执行多个线程 并行 多个事情在同一时间点内发生#xff0c;并行的发生是不会抢占资源的 并发 多个事情在一段时间内同时发生#xff0c;并发的产生会抢占资源 多线程的好处 如果为单线程计算机一次只能处理一个线程#xff0c;那么当处理的线程需…多线程 计算机在同一时间可以执行多个线程 并行 多个事情在同一时间点内发生并行的发生是不会抢占资源的 并发 多个事情在一段时间内同时发生并发的产生会抢占资源 多线程的好处 如果为单线程计算机一次只能处理一个线程那么当处理的线程需要等待获取其他资源时CPU就会处于空闲状态这时CPU就可以执行其他的线程从而提高CPU的利用率。由于一个程序的执行是由多个线程组成的相比于单线程也能提高程序的执行速度。 并不是只有当正在执行的线程需要获取其他资源时线程才能切换由于是多线程为了满足所有的线程都能被平等地执行于是采用的是分时的线程调度机制(当一个线程被执行的时间到达指定的时间后就会进行等待而CPU就会去执行其他的进程)于是会引发如下的问题。 多线程引发的问题 共享资源的安全性问题由于计算机在同一时间执行了多个线程当多个线程对同一资源进行修改时(访问同一资源)会导致变量出现问题。 例如 JVM的主内存存在一个变量int a 0;此时有两个线程A和B都要对变量a进行加一操作A线程先对变量a进行加一操作此时线程A还没有对变量a的值进行覆盖但是时间片进行了切换此时如果线程B读取主内存中的变量a那么变量a的旧值就会被线程B获取线程A和线程B最后进行结果覆盖我们就会发现此时变量a的结果只是加一了一次与正确的结果不一样。 上下文切换问题 如今我们使用的计算机都是多线程的所以我们才可以边听音乐边打游戏这是由于播放音乐线程和游戏运行线程在非常短的时间内交替执行时间短到我们无法察觉因此我们才会觉得我们是在边听音乐边打游戏但是线程进行上下文切换时也需要耗费一定的资源所以当执行的线程过多时在相同的一段时间内同一线程被执行的次数就会变少也就会导致我们觉得计算机变卡了。 并发编程 为了解决多线程引发的共享资源的安全性问题(多个线程同一个时间点访问同一资源)我们将同时访问同一资源的线程使用编程进行管理转换为线程依次对同一资源进行访问这就是并发编程。 并发编程如何实现 synchronized关键字 public class Cat { public static int age 0; public static Object lock new Object();
public synchronized void set() {// synchronized关键字修饰的方法不会同时被多个线程执行age;
}public void play() {// synchronized关键字修饰的代码块不会同时被多个线程执行synchronized (lock) {// lock的对象头中记录的是锁的状态为了保证不同线程获取的锁对象为同一个age;}
}}
ReentrantLock类 Java内存模型(Java Memory Model)JMM JMM指的是Java虚拟机在执行线程时内存的工作过程 Java虚拟机将所有的变量都存储在主内存当中当一个线程需要对主内存当中的变量进行操作时会先将主内存中的变量复制一份到线程中的工作内存当中(每一个线程都会有一个属于自己的工作内存是线程私有的)然后对工作内存中的变量副本进行操作最后用操作完成后的变量副本对主内存中的变量进行覆盖如果只是查询操作就不需要对主内存中的变量进行覆盖。 Java内存模型存在的问题 不可见性 由于线程在对变量进行操作时是在线程自己私有的工作内存中因此一个线程在对变量进行操作时另一个线程是不知道的所以另一个线程无法准确并适时地获取当前真正的变量就有可能导致对变量操作后的结果存在问题。 无序性 当线程中正在执行的代码需要等待获取其他资源时为了提高CPU的运行效率CPU就会跳过当前代码而执行后面可以执行的代码这种情况在大多数情况下都是没有问题的但是毕竟运行的顺序和程序中代码写的不一样难免会出现一些问题。 非原子性 当一个线程的时间片结束时会进行上下文的切换由于Java虚拟机中只能保证机器指令的执行是原子性的而正常的一条对数据进行更改的操作例如count就需要3条机器指令来完成 指令1获取主内存中的变量count 指令2在工作内存中执行count操作 指令3将结果赋值给主内存中 只要其中一条指令被分开执行都会导致主内存中的数据出现问题 volatile关键字 用于修饰类中的成员变量(静态和非静态)保证了这个变量在被不同线程操作时的可见性一个线程在改变了变量的值之后对于另一个线程来说是立即可见的可以禁止指令的重排序volatile不能保证对变量操作的原子性。 volatile底层实现原理 使用Memory Barrier(内存屏障) 如何保证原子性 需要保证同一个时刻多个线程在访问同一个资源时只有一个线程可以进行访问 锁 synchronized关键字 synchronized关键字用于修饰方法或者代码块修饰的方法以及代码块中的代码在同一时刻只能有一个线程进行访问synchronized关键字可以保证原子性同时由于关键字将代码块中的代码变为了一个整体所以也能够保证可见性和有序性 原子变量 原子类 CAS(Compare And Swap)比较并交换是乐观锁的一种实现方式采用的是自旋锁的思想 CAS中包含了三个操作数内存值、预估值、更新值当一个线程对主内存中的变量操作完成后需要将结果赋值给主内存时需要对内存值和预估值进行判断内存值就是当前主内存中变量的值预估值就是线程将主内存中变量的值复制到工作内存时的值如果这两个值相等就认为这个变量在线程进行结果计算时并没有发生改变这时可以直接将得到的结果赋值给主内存中的变量如果这两个值不相等那么就认为线程在对变量进行结果计算的过程中又有其他的线程对主内存中的变量进行了修改这时这个线程就需要重新获取新的内存值并重新进行一次结果计算直到内存值和预估值相等。 CAS的缺点 CAS采用自旋锁的方式(无锁)不断地重复就绪执行的状态直到内存值和预估值相等会占用一定的CPU所以如果这样的线程过多的时候就会导致CPU开销过大。 ABA问题 虽然说CAS可以通过比较内存值和预估值是否相等来判断变量是否被更改但是有一种情况即使内存值和预估值相等的情况下变量依然被修改了只不过进行多次修改后的内存值仍然与预估值是相等的 解决方法 给变量添加版本号每次在对变量进行修改时改变版本号虽然有可能内存值和预估值比较是相等的但是如果过程中发生了修改那么版本号也是不同的可以通过比较内存值和预估值以及版本号来确定变量在内存中是否更新过。
Java中锁的分类 锁的分类可以根据特性、设计、状态进行分类并不都是具体的实现 乐观锁/悲观锁 根据如何看待线程并发问题进行分类 乐观锁 乐观锁认为当一个线程访问数据时另一个线程不会对这个数据进行修改所以只会在更新数据时进行判断如果访问数据的过程中数据修改了那么就会不断尝试更新数据如果数据没有修改就直接执行赋值操作。乐观锁没有加锁。乐观锁适合读操作较多的场景不加锁性能会提升。 悲观锁 悲观锁认为当一个线程访问数据时另一个线程一定会对这个数据进行修改即使没有更改数据也会认为进行了修改。因此当多个线程对同一个数据进行访问时会进行加锁使同一时刻只能有一个线程访问同一资源。悲观锁适合写操作较多的场景。 可重入锁 同一个线程获取了外层方法的锁时在进入内层方法时会自动获取锁(前提获取的锁对象为同一个)。获取一个类中的不同方法时synchronized和ReentrantLock获取的是同一把锁。 代码举例 public static synchronized void testA() { System.out.println(“A方法”); testB(); }
public static synchronized void testB() { System.out.println(“B方法”); }
public static void main(String[] args) { new Thread(() - { testA(); }).start(); } synchronized锁是可重入锁如果不是可重入锁那么就会产生死锁。 读写锁 读写锁读读不互斥读写互斥写写互斥防止一个线程在修改数据时另一个线程读取到了更改之前数据。 ReentrantReadWriteLock是一种读写锁的实现。 分段锁 分段锁简单来说就是将锁的粒度细化对数据进行分段并给每个分段加锁提高并发效率。 Hashtable集合是线程安全的使用的是synchronized关键字对每个集合中的方法都进行加锁这样虽然是线程安全的但是同时也会降低并发效率为了提高集合的并发效率于是采用了分段锁将锁的粒度进行细分Hashtable底层实现是哈希表链表/红黑树并不是每次并发访问集合都会对哈希表中同一个位置进行访问所以对哈希表的每一个位置加锁保证同一时刻不会对哈希表中同一个位置的元素进行访问。 自旋锁 线程在获取锁失败后不断循环尝试获取锁不断尝试过程中该线程不会被切换这种情况只适合低并发的场景因为自旋锁会占用CPU的资源如果并发数量过多会导致CPU占用过大。自旋锁适合加锁时间(获取锁的线程的执行时间)非常短的场景这样不断尝试获取锁的线程的等待时间也会很短相比于将线程 共享锁/独占锁 共享锁锁对象可以被多个线程持有读锁就是共享锁 独占锁锁对象只能被同一线程持有写锁就是独享锁 共享锁可以提高对同一个数据的并发读操作的效率 公平锁/非公平锁 非公平锁多个线程谁先获取锁对象谁就可以执行 公平锁多个线程按照先来先服务的原则依次获取锁对象 synchronized锁是非公平锁 ReentrantLock锁默认是非公平的也可以实现公平锁 ReentrantLock底层实现类AbstractQueuedSynchronizer(AQS)中存在队列将没有获取锁对象的线程存放入队列中来实现公平锁。 偏向锁/轻量级锁/重量级锁(锁的状态) 每一个对象都会有对象头锁的状态就存放在对象头中的Mark Word区域中 偏向锁 锁的状态一开始是无锁状态(没有一个线程访问同步代码块)当同步代码块一直被同一个线程访问时锁的状态就会变为偏向锁的状态这时锁对象会将该线程的id记录在对象头中降低之后再次获取锁对象的成本提高运行效率。 轻量级锁 当锁是偏向锁的状态时其他线程获取不到锁对象此时锁状态就会升级为轻量级锁此时访问不到锁对象的线程会采用自旋锁的方式不断尝试获取锁对象。 重量级锁 当锁是轻量级锁的状态时采用自旋锁方式获取锁对象的线程经过一定的自旋次数仍然没有获取到锁对象就会进入阻塞状态此时锁状态上升位重量级锁进入阻塞状态的线程只能等待CPU的调度。 对象结构 对象在Java虚拟机内存中有三部分区域对象头、实例数据和对齐填充 对象头中有一块区域Mark Word用于存放对象运行时的数据如HashCode、经历垃圾回收的次数、锁状态标志(用于记录锁对象是否正在被线程使用)、线程持有的锁状态(偏向、轻量级、重量级)、偏向锁的线程id Synchronized锁实现 synchronized关键字用于修饰同步代码块和同步方法同步方法和同步代码块都是通过moniterenter和moniterexit指令实现同步方法在调用时会有一个ACC_SYNCHRONIZED标识用于说明该方法为同步方法所以在方法开始执行时执行moniterenter指令当方法执行完成后执行moniterexit指令。同步代码块没有标识只是会在进入同步代码块之前指令moniterenter指令在同步代码块执行结束时执行moniterexit指令。 在执行moniterenter指令时线程会尝试获取该同步代码块或同步方法的锁对象如果获取成功就将锁对象的计数器加一在执行moniterexit指令时会将锁对象的计数器减一当锁对象的计数器为0时该锁对象就会被释放也就是说同步代码块或同步方法中的内容已经执行完成。 AQS(AbstractQueuedSynchronizer) ReentrantLock锁、ReentrantReadWriteLock锁都是基于AQS抽象类中的方法实现的AQS位于java.util.concurrent.locks包中也就是说AQS是解决线程安全问题的锁的实现类AQS是抽象类但是其中没有抽象方法。 AQS类中有一个int类型成员变量state用于线程判断锁对象是否正在被使用默认情况下state的值为0当有线程正在使用锁对象时state的值变为1。使用volatile关键字修饰state变量可以确保可见性和有序性所以为了确保对变量修改的原子性AQS类中提供了特定的原子性的方法保证对变量的操作是线程安全的。 当多个线程获取锁对象时AQS会将获取不到锁对象的线程存放到队列当中队列底层是由双向链表实现链表的每一个节点(NodeAQS的内部类)封装着一个线程链表的头部为获取到锁对象的线程。 acquire()方法 用于获取锁对象 方法中的第一步调用tryAcquire(int age)方法尝试获取锁对象如果获取成功返回trueacquire()方法执行完毕否则返回false于是尝试获取锁对象的线程就会被封装到Node节点中并插入到双向链表的尾部。 AQS的锁存在独占锁和共享锁 独占锁ReentrantLock锁就是独占锁 共享锁ReentrantReadWriteLock锁中的读锁就是共享锁 ReentrantLock锁的实现 ReentrantLock锁中又分为公平锁和非公平锁他们的区别在于公平锁是当线程获取锁对象时直接将线程放入队列中进行等待而非公平锁时当线程获取锁对象时会先尝试获取锁对象如果尝试获取不到才会将线程放入队列中进行等待。 如下为公平锁和非公平锁获取锁的实现代码 final void lock() {// 公平锁获取锁对象 acquire(1);// 排队 }
final void lock() {// 非公平锁获取锁对象 if (compareAndSetState(0, 1))// 先尝试获取锁对象底层使用AQS实现根据变量state判断是否可以获取锁对象 setExclusiveOwnerThread(Thread.currentThread()); else acquire(1);// 排队 } 为什么公平锁和非公平锁都需要将线程进入等待队列中 进入等待队列中的线程会被阻塞当锁释放后系统会对阻塞的线程进行唤醒使线程获取锁对象如果阻塞的线程没有进入等待队列系统就会随机唤醒阻塞线程这样有可能导致有些阻塞的线程永远不会被唤醒。 ReentrantLock悲观锁、公平锁(可以实现非公平锁)、互斥锁、可重入锁 Synchronized悲观锁、非公平锁、互斥锁、可重入锁 JUC常用类(Java.util.concurrent) ConcurrentHashMap ConcurrentHashMap和HashMap一样都是线程安全的但是ConcurrentHashMap的效率高于HashMap。原因是因为ConcurrentHashMap采用分段锁的思想降低锁的粒度提高了执行效率。 HashMap在方法中使用synchronized关键字使方法变为同步方法多个线程不能同时调用同一个方法而ConcurrentHashMap使用分段锁将锁的粒度变为链表或者红黑树这样多个线程在同一时刻就可以调用同一个方法提高了执行效率。 JDK1.8之后放弃分段锁的原因 分段锁是给链表或者红黑树加锁当集合中的元素越来越多时会导致链表变得越来越长进而会使锁的粒度变得越来越大从而导致分段锁的性能变低。 分段锁是给Hash表中的每一个节点都添加锁不管该索引位置下是否存在元素都会存在锁有可能该索引下一直没有元素导致内存空间的浪费。 JDK1.8之后放弃了分段锁而使用粒度更小的Node锁在添加元素时首先判断该索引位置下是否存在元素如果不存在元素就直接添加元素如果存在元素就给该位置下的Node头节点添加Node锁(synchronized锁)确保添加时的原子性。 ConcurrentHashMap不支持存储null键和null值的原因 不支持null值是为了消除歧义如果键值对的值为null那么在使用键获取对应的值时如果返回值为null我们不清楚是输入键所对应的值为null还是输入键因为在集合中不存在所以返回null。 CopyOnWriteArrayList CopyOnWriteArrayList相比于ArrayList是线程安全的虽然Vector同样也是线程安全的但是CopyOnWriteArrayList的效率要高于Vector。 Vector给每一个方法都添加了synchronized关键字同一时刻只能有一个线程访问同一个方法CopyOnWriteArrayList允许同一时刻有多个线程进行读操作但是同一时刻只能有一个线程进行写操作CopyOnWriteArrayList在进行写操作时会先对集合中的底层数组进行复制一份并对复制出来的数组进行数据修改操作当修改完成后再使用修改完成后的数组对原数组进行覆盖这样在多线程的情况下当一个线程对集合进行写操作的同时其他线程还能对集合进行读操作提高了运行效率。CopyOnWriteArrayList使用ReentrantLock锁实现对集合写操作的并发执行。 CopyOnWriteArraySet CopyOnWriteArraySet是基于CopyOnWriteArrayList实现的不能存储重复元素。 CountDownLatch CountDownLatch允许一个线程在等待其他线程执行完毕之后在去执行在创建对象时指定参数用于表示线程等待其他线程的数量。