基本概念
多线程:
指的是这个程序(一个进程)运行时产生了不止一个线程
并行:
多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
并发:
通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。
线程安全:
指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只需要关注系统的内存,cpu是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果。
同步:
通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。
线程的状态
NEW(新建状态)
当一个线程创建以后,就处于新建状态。那什么时候这个状态会改变呢?只要它调用的start()方法,线程就进入了锁池状态。
BLOCKED(锁池)
进入锁池以后就会参与锁的竞争,当它获得锁以后还不能马上运行,因为一个单核CPU在某一时刻,只能执行一个线程,所以他需要操作系统分配给它时间片,才能执行。
RUNNABLE(运行状态)
当一个持有对象锁的线程获得CPU时间片以后,开始执行这个线程,此时叫做运行状态。
TIMED_WAITING(定时等待)、WAITING(等待)
处于运行状态的线程还可能调用wait()方法、或者带时间参数的wait(long milli)方法。这时候线程就会将对象锁释放,进入等待队列里面(如果是调用wait()方法则进入等待状态,如果是调用带时间参数的则进入定时等待状态)
TERMINATED(终止、结束)
当一个线程正常执行完,那么就进入终止(死亡)状态。系统就会回收这个线程占用的资源。
注意:1.当线程调用sleep()方法或当前线程中有其他线程调用了带时间参数的join()方法的时候进入了定时等待状态(TIMED_WAITING)
2.当其他线程调用了不带时间参数的join()(join内部调用的是sleep,所以可看成sleep的一种)方法时进入等待状态(WAITING)。
3.当线程遇到I/O的时候还是运行状态(RUNNABLE)
4.当一个线程调用了suspend()方法挂起的时候它还是运行状态(RUNNABLE)。
synchronized、Lock
他们是应用于同步问题的人工线程调度工具,wait/notify必须存在于synchronized块中。并且,这三个关键字针对的是同一个监视器(某对象的监视器)。这意味着wait之后,其他线程可以进入同步块执行。
同步原理
JVM规范规定JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有详细说明,但是方法的同步同样可以使用这两个指令来实现。monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处, JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个 monitor 与之关联,当且一个monitor 被持有后,它将处于锁定状态。线程执行到 monitorenter 指令时,将会尝试获取对象所对应的 monitor 的所有权,即尝试获得对象的锁。
synchronized的使用
锁的本质是对象实例,对于非静态方法来说,Synchronized 有两种呈现形式,Synchronized方法体和Synchronized语句块。两种呈现形式本质上的锁都是对象实例。
1.锁定实例
public class SynchronizeDemo {
public void doSth1() {
/**
* 锁对象实例 synchronizeDemo
*/
synchronized (synchronizeDemo){
try {
System.out.println("正在执行方法");
Thread.sleep(10000);
System.out.println("正在退出方法");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void doSth2() {
/**
* 锁对象实例 this 等同于 synchronizeDemo
*/
synchronized (this){
try {
System.out.println("正在执行方法");
Thread.sleep(10000);
System.out.println("正在退出方法");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
synchronized块中的方法获取了lock实例的monitor,如果实例相同,那么只有一个线程能执行该块内容
2.直接用于方法
public synchronized void doSth3() {
/**
* 表面呈现是锁方法体,实际上是synchronized (this) ,等价于上面
*/
try {
System.out.println("正在执行方法");
Thread.sleep(10000);
System.out.println("正在退出方法");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
相当于上面代码中用lock来锁定的效果,实际获取的是Thread1类的monitor。更进一步,如果修饰的是static方法,则锁定Synchronized 的Class对象
Lock的使用
lock: 在java.util.concurrent包内。共有三个实现:
1.ReentrantLock
2.ReentrantReadWriteLock.ReadLock
3.ReentrantReadWriteLock.WriteLock
lock的主要目的是和synchronized一样,但是lock更灵活
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。Sychronized的锁是非公平的,ReentrantLock默认情况下也是非公平的,但可以通过带boolean值的构造函数要求使用公平锁;
锁绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象,而在Synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要多于一个条件关联的时候,就不得不额外添加一个锁,而ReentrantLock无需这样做,只需要多次调用newCondition()方法即可。
ReentrantLock
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 有界阻塞队列<br/>
* 当队列为空,队列将会阻塞删除并获取操作的线程,直到队列中有新元素;<br/>
* 当队列已满,队列将会阻塞添加操作的线程,直到队列有空位置;
* <p>
* Created by LeonWong on 16/4/29.
*/
public class BoundedQueue<T> {
private Object[] items;
// 添加的下标,删除的下标和数组当前数量
private int addIndex, removeIndex, count;
private Lock lock = new ReentrantLock();
private Condition notEmpty = lock.newCondition();
private Condition notFull = lock.newCondition();
public BoundedQueue() {
items = new Object[5];
}
public BoundedQueue(int size) {
items = new Object[size];
}
/**
* 添加一个元素,数组满则添加线程进入等待状态
*
* @param t
* @throws InterruptedException
*/
public void add(T t) throws InterruptedException {
lock.lock();
try {
while (items.length == count) {
System.out.println("添加队列--陷入等待");
notFull.await();
}
items[addIndex] = t;
addIndex = ++addIndex == items.length ? 0 : addIndex;
count++;
notEmpty.signal();
} finally {
lock.unlock();
}
}
/**
* 删除并获取一个元素,数组空则进入等待
*
* @return
* @throws InterruptedException
*/
public T remove() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
System.out.println("删除队列--陷入等待");
notEmpty.await();
}
Object tmp = items[removeIndex];
items[removeIndex] = null;// 这一步可以有可无
removeIndex = ++removeIndex == items.length ? 0 : removeIndex;
count--;
notFull.signal();
return (T) tmp;
} finally {
lock.unlock();
}
}
public Object[] getItems() {
return items;
}
public void setItems(Object[] items) {
this.items = items;
}
public int getAddIndex() {
return addIndex;
}
public void setAddIndex(int addIndex) {
this.addIndex = addIndex;
}
public int getRemoveIndex() {
return removeIndex;
}
public void setRemoveIndex(int removeIndex) {
this.removeIndex = removeIndex;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
}
ReentrantReadWriteLock
允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问。相对于排他锁,提高了并发性。在实际应用中,大部分情况下对共享数据(如缓存)的访问都是读操作远多于写操作,这时ReentrantReadWriteLock能够提供比排他锁更好的并发性和吞吐量。
进程切换导致的系统开销
Java的线程是直接映射到操作系统线程之上的,线程的挂起、阻塞、唤醒等都需要操作系统的参与,因此在线程切换的过程中是有一定的系统开销的。在多线程环境下调用Synchronized方法,有可能需要多次线程状态切换,因此可以说Synchronized是在Java语言中一个重量级操作。
虽然如此,JDK1.6版本后还是对Synchronized关键字做了相关优化,加入锁自旋特性减少系统线程切换导致的开销,几乎与ReentrantLock的性能不相上下,因此建议在能满足业务需求的前提下,优先使用Sychronized。
volatile
多线程的内存模型:main memory(主存)、working memory(线程栈),在处理数据时,线程会把值从主存load到本地栈,完成操作后再save回去(volatile关键词的作用:每次针对该变量的操作都激发一次load and save)。
使用volatile关键字仅能实现对原始变量(如boolen、 short 、int 、long等)操作的原子性,但需要特别注意, volatile不能保证复合操作的原子性,即使只是i++,实际上也是由多个原子操作组成:read i; inc; write i,假如多个线程同时执行i++,volatile只能保证他们操作的i是同一块内存,但依然可能出现写入脏数据的情况。
volatile和synchronized的区别
1.volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
2.volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
3.volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
4.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
5.volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
常用方法
Thread.yield()
当前线程可转让cpu控制权,让别的就绪状态线程运行(切换)
Thread.sleep()
暂停一段时间
join()
在一个线程中调用other.join(),将等待other执行完后才继续本线程。
interrupte()
中断线程
中断是一个状态!interrupt()方法只是将这个状态置为true而已。所以说正常运行的程序不去检测状态,就不会终止,而wait等阻塞方法会去检查并抛出异常。如果在正常运行的程序中添加while(!Thread.interrupted()) ,则同样可以在中断后离开代码体
future模式
使用步骤
(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
多线程控制类
1.ThreadLocal类
https://www.jianshu.com/p/e465ec03f326
2.原子类(AtomicInteger、AtomicBoolean……)
3.容器类
BlockingQueue
阻塞队列。该类是java.util.concurrent包下的重要类,通过对Queue的学习可以得知,这个queue是单向队列,可以在队列头添加元素和在队尾删除或取出元素
除了传统的queue功能(表格左边的两列)之外,还提供了阻塞接口put和take,带超时功能的阻塞接口offer和poll。put会在队列满的时候阻塞,直到有空间时被唤醒;take在队 列空的时候阻塞,直到有东西拿的时候才被唤醒。用于生产者-消费者模型尤其好用
常见的阻塞队列有:
ArrayListBlockingQueue
LinkedListBlockingQueue
DelayQueue
SynchronousQueue
ConcurrentHashMap
高效的线程安全哈希map
4.线程池
https://www.jianshu.com/p/bf2368937918
5. 信号量(Semaphore)
信号量是一个非负整数(车位数),所有通过它的线程(车辆)都会将该整数减一(通过它当然是为了使用资源),当该整数值为零时,所有试图通过它的线程都将处于等待状态。在信号量上我们定义两种操作: Wait(等待) 和 Release(释放)。 当一个线程调用Wait(等待)操作时,它要么通过然后将信号量减一,要么一直等下去,直到信号量大于一或超时。Release(释放)实际上是在信号量上执行加操作,对应于车辆离开停车场,该操作之所以叫做“释放”是因为加操作实际上是释放了由信号量守护的资源。