- 1,thread
2,synchronize
3,lock 和 condition
4,volatite
5,atomic
6,executorService thread Pool
7,并发容器
8,aqs
9,线程安全
0,程序状态,new,创建后没有启动
runnable,启动了,正在运行,或者在等待cpu时间片
blocked,等待获得一个排他锁,若其他线程释放锁就回结束这个状态
wait,object.wait和thread.join会让线程无限等待,直到object.notify
timewait,thread.sleep和设置了时间参数的object.wait和thread.join
1,thread有三种实现方法,继承thread,和实现runnable,和callable<t>,Java只能单继承,尽量使用runnable,runnable不用继承整个thread类,节省资源,还可以共享变量
ExecutorService executorService = Executors.newCacheThrealPool(); executorService.execute(new MyRunnable());newFixedThreadPool(int) SingleThreadExecutor()
静态方法,Thread.currentThread()是指当前运行的线程,在比如在构造函数的时候是主线程
thread1 = new Thread(thread2)的时候,run是用thread2,this指针是指thread2,Thread.currentThread()是thread1,thread1这里不算活,用alive检查
停止线程:1,stop,2,退出标记3,interrupt,interrupt打断,this.interrupte()由false变true,第二次会复位为false,注意是当前线程,isInterrupted测试目标线程,不会重置为false
通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException
sleep不释放锁,wait释放锁
2,synchronize,
多个对象多把锁(加在方法的synchronize算对象)因为再多个对象同步块中的内存是算多个内存的,多个线程各自访问各种对象内存中方法的变量,只有共享资源才有需要加锁,就是只有在一个对象的时候,多个线程都需要访问同一个变量,需要加锁保证编程的准确性,一个线程占用锁的时候,另外一个线程可以调用其他不加锁的方法,但不可以调用同是synchronize的方法,这是因为synchronize只锁住线程需要争夺资源的那部分,也就是共享的变量值,
可重入,一个线程使用获得一个锁的时候,在执行同步块的资源时,调用本类的其他synchronize方法的时候,可以重入地获得锁,原理是所有请求的线程排成一个队列,锁没有线程请求的时候,状态位为0,当一个线程请求锁的时候状态位1,当线程需要调用本类的其他synchronize的方法的时候,状态位+1变成2,释放锁的时候状态位减1, 减到0的时候释放锁,后面堵塞状态的线程可按照fifo获得线程(公平锁),或者争抢一个锁。防止死锁,不能重入的话,就会自己等待自己释放锁,死循环
synchronized(x)对x对象加锁,synchronize(this)对自己加锁,和在方法上加锁是一样的,静态方法加锁就是对class文件加锁,也就是class锁住,对象没有锁住,对象锁是可以异步的,synchronized(xxx.class)同理
wait释放锁,sleep不释放,join底层调用了wait。
wait和notify需要再同步块里面进行:不使用同步块的话,并发的情况下程序的执行顺序是随机的,可能出现先notify还没有wait的情况,notify也可以指定对象,因为notify本来是随机的,会有java.lang.IllegalMonitorStateException异常
3,ReentrantLock & condiction
jvm的synchronize相当于lock的this.lock,this.unlock
object的wait(),wait(x),notify(),notifyAll(),分别等于Condition的await(),await(x,y),signal(),signalAll()
ReentrantLock的优点:
1,某个线程在等待一个锁的控制权的这段时间需要中断
tryLock(),tryLock(long timeout,TimeUnit unit),可以尝试获得锁,没有就返回false,可以处理其他事情
2,需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程
Lock lock = new ReentrantLock(); Condition condition1 = lock.newCondition(); Condition condition2 = lock.new Condition(); condition1.await() condition2.await() condition1.signal() condition2.signal()
3,具有公平锁功能,每个到来的线程都将排队等候
ReentrantLock lock = new ReentrantLock(true),false为不公平
4,读写锁
Lock lock = new ReentrantReadWriteLock(); lock.readLock().lock(); lock.writeLock().lock()
synchronize优点
1、线程自旋和适应性自旋 :互斥同步进入阻塞状态的开销都很大,应该尽量避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。自旋锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。
2、锁消除 :锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。再jdk1.5前,再一个方法内字符串相加,s1+s2+s3,会String 是一个不可变的类,编译器会对 String 的拼接自动优化。在 JDK 1.5 之前,会转化为 StringBuffer 对象的连续 append() ,每个 append() 方法中都有一个同步块。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说,sb 的所有引用永远不会逃逸到 concatString() 方法之外,其他线程无法访问到它,因此可以进行消除。
3、锁粗化 :如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。连续的 append() 方法就属于这类情况。如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了。
4、轻量级锁:锁拥有了四个状态:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。对象头的内存布局,这些数据被称为 Mark Word。其中 tag bits 对应了五个状态,根据状态不同,对象头的内容也会有不同。
当尝试获取一个锁对象时,如果锁对象标记为 0 01,说明锁对象的锁未锁定(unlocked)状态。此时虚拟机在当前线程的虚拟机栈中创建 Lock Record,然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00,表示该对象处于轻量级锁状态。如果 CAS 操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈,如果是的话说明当前线程已经拥有了这个锁对象,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁。轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。
轻量级锁的释放:用mark work中指着线程的锁记录的displace mark work进行cas操作,替换成功,则整个同步块完成,替换失败,说明同步互斥期间有其他线程尝试获得过锁,那就要再释放锁的时候,也要唤醒阻塞的线程
5、偏向锁:偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要。
当锁对象第一次被线程获得的时候,进入偏向状态,标记为 1 01。同时使用 CAS 操作将线程 ID 记录到 Mark Word 中,如果 CAS 操作成功,这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作。
当有另外一个线程去尝试获取这个锁对象时,需要看对象资源是否处于被锁定状态,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态。
4,volatite
先说Java内存模型,处理器上的寄存器的读写的速度比内存快,如果多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致,需要一些协议来解决这个问题。Java 内存模型试图屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。在Java内存模型中所有的变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝。线程只能直接操作工作内存中的变量,不同线程之间的变量值传递需要通过主内存来完成。
Java定义了工作内存和主内存中交互操作的8个原子操作
- read:把一个变量的值从主内存传输到工作内存中
- load:在 read 之后执行,把 read 得到的值放入工作内存的变量副本中
- use:把工作内存中一个变量的值传递给执行引擎
- assign:把一个从执行引擎接收到的值赋给工作内存的变量
- store:把工作内存的一个变量的值传送到主内存中
- write:在 store 之后执行,把 store 得到的值放入主内存的变量中
- lock:作用于主内存的变量
- unlock
原子性:Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如对一个 int 类型的变量执行 assign 赋值操作,这个操作就是原子性的。load、assign、store 这一系列操作整体上看不具备原子性,那么在 T1 修改 cnt 并且还没有将修改后的值写入主内存,T2 依然可以读入旧值。可以看出,这两个线程虽然执行了两次自增运算,但是主内存中 cnt 的值最后为 1 而不是 2。因此对 int 类型读写操作满足原子性只是说明 load、assign、store 这些单个操作具备原子性。
可见性:指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。
- volatile:在read操作之前和write操作之后,都回加一个内存屏障,强制把变量刷新回主内存和强制每次使用前从主内存刷新变量,而不是使用寄存器的缓冲
- synchronized,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。
有序性:在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
- volatile:借用4条内存屏障,LoadLoad ,StoreStore ,LoadStore,StoreLoad
- synchronized,使用互斥同步,相当与每次只有单线程在跑这个程序
5,atomic
cas:当线程写数据的时候,先对内存中要操作的数据保留一份旧值,真正写的时候,比较当前的值是否和旧值相同,如果相同,则进行写操作。如果不同,说明在此期间值已经被修改过,则重新尝试。
final void set(long newValue) // 获取当前值 final long get() // 以原子方式将当前值减 1,并返回减1后的值。等价于“--num” final long decrementAndGet() // 以原子方式将当前值减 1,并返回减1前的值。等价于“num--” final long getAndDecrement() // 以原子方式将当前值加 1,并返回加1后的值。等价于“++num” final long incrementAndGet() // 以原子方式将当前值加 1,并返回加1前的值。等价于“num++” final long getAndIncrement() // 以原子方式将delta与当前值相加,并返回相加后的值。 final long addAndGet(long delta) // 以原子方式将delta添加到当前值,并返回相加前的值。 final long getAndAdd(long delta)
6,executorService thread Pool
好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
坏处:
4种常用线程池:
- Executors.newCachedThreadPool()创建一个可根据实际情况调整线程数量的可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
- Executors.newFixedThreadPool() 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
- Executors.newScheduledThreadPool()创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
- Executors.newSingleThreadExecutor()创建一个使用单个worker线程的Executor,以无界队列方式来运行该线程。它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
关键类:
Executor
—— 执行任务的简单接口,提供execute()ExecutorService
—— 一个较复杂的接口,包含额外方法来管理任务和 executor 本身,增加了shutDown(),shutDownNow(),invokeAll(),invokeAny()和submit()等方法。ScheduledExecutorService
—— 扩展自ExecutorService
,增加了执行任务的调度方法- ThreadPoolExecutor --------
- corePoolSize:表示线程池中线程的个数,初始时并没有线程,每到来一个任务便创建一个,当创建的数目超过这个数时,便进入缓存队列
-
- 如果运行的线程少于 corePoolSize,则创建新线程来处理任务,即使线程池中的其他线程是空闲的;
- 如果线程池中的线程数量大于等于 corePoolSize 且小于 maximumPoolSize,则只有当workQueue满时才创建新的线程去处理任务;
- 如果设置的corePoolSize 和 maximumPoolSize相同,则创建的线程池的大小是固定的,这时如果有新任务提交,若workQueue未满,则将请求放入workQueue中,等待有空闲的线程去从workQueue中取任务并处理;
- 如果运行的线程数量大于等于maximumPoolSize,这时如果workQueue已经满了,则通过handler所指定的策略来处理任务;
- maximumPoolSize: 表示线程池中最多可创建的线程数,若不做限制,则线程池与一般的来一个任务便创建一个线程没有任何区别
- keepAliveTime:表示线程没有任务执行时存活的时间,这个限制主要用于超过corePoolSize的那一部分线程
- unit:时间单位,都是TimeUnit的一些静态参数
- workQueue:阻塞队列,用来存储等待执行的任务。BlockingQueue是一个接口,常用的实现类下面介绍
- ThreadFactory:线程工厂,主要定义一些创建线程的动作
- RejectedExecutionHandler:当拒绝处理任务时的策略。
7,并发容器 :
9,线程安全策略
1)、不可变对象默认是线程安全的,因为他们一旦被创建就不会被修改。比如 String 是不可变对象,是线程安全的。只读、final 类型的变量也是线程安全的