多线程知识点总结
1.进程与线程
1.1什么是进程
程序是静止的,只有真正运行时的程序,才被称之为进程。
单核CPU在任何时间点上,只能运行一个进程:宏观并行,微观串行。
1.2什么是线程
线程,又称轻量级进程(Light Weight Process)。程序中的一个顺序控制流程,同时也是CPU调度的基本单位。进程由多个线程组成,彼此间完成不同的工作,交替执行,成为多线程。
迅雷是一个进程,当中的多个下载任务即是多个线程。
Java虚拟机是一个进程,当中默认包含主线程(Main),可通过代码创建多个独立线程,与Main并发执行。
1.3进程和线程的区别
- 进程是操作系统资源分配的基本单位,而线程是CPU的基本调度单位。
- 一个程序运行后至少有一个进程。
- 一个进程可以包含多个线程,但是至少需要有一个线程。
- 进程间不能共享数据段地址,但同进程之间可以。
2.线程的组成
- 任何一个线程都具有基本的组成部分:
- CPU时间片:操作系统(OS)会为每个线程分配执行时间
- 运行数据:
- 堆空间:存储线程需使用的对象,多个线程可以共享堆中的对象。
- 栈空间:存储线程需使用的局部变量,每个线程都拥有独立的栈。
- 线程的逻辑代码。
3.线程的创建
3.1创建线程(1)
- 创建线程的第一种方式:
- 1.继承Thread类
- 2.重写run()方法
- 3.创建子类对象
- 4.调用start()方法
public class TestCreateThread {
public static void main(String[] args) {
MyThread t1 = new MyThread();//3.创建子类对象
t1.start();//4.调用start()方法
}
}
class MyThread extends Thread {//1.继承Thread类
@Override
public void run() {//2.重写run()方法
for(int i = 1; i <= 50; i++) {
System.out.println("MyThread:" + i);
}
}
}
3.2创建线程(2)
- 创建线程的第二种方式:
- 1.实现Runnable接口
- 2.重写run()方法
- 3.创建实现类对象
- 4.创建线程对象
- 5.调用start()方法
public class TestCreateThread {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();//3.创建实现类对象
Thread t2 = new Thread(mr);//4.创建线程对象
t2.start();//5.调用start()方法
}
}
class MyRunnable implements Runnable {//1.实现Runnable接口
pulbic void run() {//2.重写run()方法
for(int i = 1; i <= 50; i++){
System.out.println("MyRunnable:" + i);
}
}
}
4.线程的状态
4.1基本状态
常见方法
- 休眠:
- public static void sleep(long millis)
- 当前线程主动休眠millis毫秒。
- 放弃:
- public static void yield()
- 当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片。
- 插队:
- public final void join()
- 允许其他线程加入到当前线程中。
4.2等待状态
4.3阻塞状态
5.线程同步
5.1线程安全问题
- 需求:A线程将"Hello"存入数组的第一个空位;B线程将"World"存入数组的第一个空位。
public class Test {
public static void main(String[] args) throws InterruptedException {
String[] s = new String[5];
Runnable task = new Runnable() {
@Override
public void run() {
s[0] = Thread.currentThread().getName();
}
};
Thread t1 = new Thread(task,"Hello");
Thread t2 = new Thread(task,"World");
t1.start();
t2.start();
t1.join();
t2.join();
for (String string : s) {
System.out.println(string);
}
}
}
- 线程不安全
- 当多线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致。
- 临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性。
- 原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可打乱或缺省。
5.2同步代码块
同步代码块:
synchronized(临界资源对象){//对临界资源对象加锁
//代码(原子操作)
}
注:
- 每个对象都有一个互斥锁标记,用来分配给线程的。
- 只有拥有对象互斥锁标记的线程,才能进入对该对象加锁的同步代码块。
- 线程退出同步代码块时,会释放响应的互斥锁标记。
5.3同步方法
同步代码块:
synchronized 返回值类型 方法名称(形参列表){//对当前对象(this)加锁
//代码(原子操作)
}
注:
- 只有拥有对象互斥锁标记的线程,才能进入该对象加锁的同步方法中。
- 线程退出同步方法时,会释放相应的互斥锁标记。
5.4线程同步规则
- 注意:
- 只有在调用包含同步代码块的方法,或者同步方法时,才需要对象的锁标记。
- 如调用不包含同步代码块的方法,或普通方法时,则不需要锁标记,可直接调用。
- 已知JDK中线程安全的类:
- StringBuffer
- Vector
- Hashtable
- 以上类中的公开方法,均为synchronized修饰的同步方法。
6.线程通信
6.1方法
- 等待:
- public final void wait()
- public final void wait(long timeout)
- 必须在对obj加锁的同步代码块中。在一个线程中,调用obj.wait()时,此线程会释放其拥有的所有锁标记。同时此线程阻塞在o的等待队列中。释放锁,进入等待队列。
- 通知:
- public final void notify()
- public final void notifyAll()
- 必须在对obj加锁的同步代码块中。从obj的Waiting中释放一个或全部线程。对自身没有任何影响。
6.2经典问题
- 死锁
- 当第一个线程拥有A对象锁标记,并等待B对象锁标记,同时第二个线程拥有B对象锁标记,并等待A对象锁标记时,产生死锁。
- 一个线程可以同时拥有多个对象的锁标记,当线程阻塞时,不会释放已经拥有的锁标记,由此可能造成死锁。
- 生产者、消费者:
- 若干个生产者在生产产品,这些产品将提供给若干个消费者去消费,为了使生产者和消费者能并发执行,在两者之间设置一个能存储多个产品的缓冲区,生产者将生产者将生产的产品放入到缓冲区中,消费者从缓冲区中取走产品进行消费,显然生产者和消费者之间必须保持同步,即不允许消费者到一个空的缓冲区中取产品,也不允许生产者向一个满的缓冲区中放入产品。
生产者消费者实例代码:
public class Bread {
private int id;
private String productName;
public Bread() {
super();
}
public Bread(int id,String productName) {
super();
this.id = id;
this.productName = productName;
}
public int getId() {
return id;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public void setId(int id) {
this.id = id;
}
}
public class BreadCon {
private Bread[] con = new Bread[6];
private int index = 0;
public synchronized void input(Bread bread) {
while (index >= 6) {
try {
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
con[index] = bread;
System.out.println(Thread.currentThread().getName()+"生产了第"+bread.getId()+"号面包");
index++;
this.notifyAll();
}
public synchronized void output() {
while (index <= 0) {
try {
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
index--;
Bread bread = con[index];
System.out.println(Thread.currentThread().getName()+"消费了第"+bread.getId()+"号面包,"+"生产者是"+bread.getProductName());
this.notifyAll();
}
}
public class Product implements Runnable {
private BreadCon con;
public Product(BreadCon con) {
super();
this.con = con;
}
public BreadCon getCon() {
return con;
}
public void setCon(BreadCon con) {
this.con = con;
}
@Override
public void run() {
for (int i = 0; i < 30; i++) {
con.input(new Bread(i, Thread.currentThread().getName()));
}
}
}
public class Consume implements Runnable {
private BreadCon con;
public Consume(BreadCon con) {
super();
this.con = con;
}
public BreadCon getCon() {
return con;
}
public void setCon(BreadCon con) {
this.con = con;
}
@Override
public void run() {
for (int i = 0; i < 30; i++) {
con.output();
}
}
}
public class TestBread {
public static void main(String[] args) {
BreadCon con = new BreadCon();
Product product = new Product(con);
Consume consume = new Consume(con);
Thread xiaoming = new Thread(product, "小明");
Thread xiaozhang = new Thread(product, "小张");
Thread xiaoli = new Thread(consume, "小丽");
Thread xiaohong = new Thread(consume, "小红");
xiaoming.start();
xiaozhang.start();
xiaoli.start();
xiaohong.start();
}
}
7.高级多线程
7.1线程池
7.1.1线程池概念
- 现有问题:
- 线程是宝贵的内存资源、单个线程约占1MB空间,过多分配易造成内存溢出。
- 频繁的创建及销毁线程会增加虚拟机回收频率、资源开销,造成程序性能下降。
- 线程池:
- 线程容器,可设定线程分配的数量上限。
- 将预先创建的线程对象存入池中,并重用线程池中的线程对象。
- 避免频繁的创建和销毁。
7.1.2线程池原理
- 将任务提交给线程池,由线程池分配线程、运行任务,并当前任务结束后复用线程。
7.1.3获取线程池
- 常用的线程池接口和类(所在包java.util.concurrent):
- Executor:线程池的顶级接口。
- ExecutorService:线程池接口,可通过submit(Runnable task)提交任务代码。
- Executors工厂类:通过此类可以获得一个线程池。
- 通过newFixedThreadPool(int nThreads)获取固定数量的线程池。参数:指定线程池中线程的数量。
- 通过newCachedThreadPool()获得动态数量的线程池,如不够则创建新的,无上限。
7.2Callable接口
public interface Callable{
public V call() throws Exception;
}
- JDK1.5加入,与Runnable接口类似,实现之后代表一个线程任务。
- Callable具有泛型返回值,可以声明异常。
需求:使用两个线程,并发计算150、51100的和,再进行汇总统计。
public class TestCallable {
public static void main(String[] args) throws Exception {
//1.创建Callable对象
Callable<Integer> c1 = new Callable<Integer>() {
private Integer sum = 0;
@Override
public Integer call() throws Exception {
for (int i = 1; i <= 50; i++) {
sum += i;
}
return sum;
}
};
Callable<Integer> c2 = new Callable<Integer>() {
private Integer sum = 0;
@Override
public Integer call() throws Exception {
for (int i = 51; i <= 100; i++) {
sum += i;
}
return sum;
}
};
//2.把Callable对象转成可执行任务
FutureTask<Integer> task1 = new FutureTask<Integer>(c1);
FutureTask<Integer> task2 = new FutureTask<Integer>(c2);
//3.创建线程
Thread t1 = new Thread(task1);
Thread t2 = new Thread(task2);
//4.启动
t1.start();
t2.start();
//5.获得结果(等待call()方法执行完毕)
Integer result = task1.get() + task2.get();
System.out.println("计算结果是:"+result);
}
}
使用Callable结合线程池完成计算:
public class TestCallableThreadPool {
public static void main(String[] args) throws InterruptedException, ExecutionException {
//1.创建线程池
ExecutorService es = Executors.newFixedThreadPool(2);
//2.提交任务 Future:表示将要执行完任务的结果
Future<Integer> f1 = es.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 50; i++) {
sum += i;
}
return sum;
}
});
Future<Integer> f2 = es.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 51; i <= 100; i++) {
sum += i;
}
return sum;
}
});
//3.获取结果
System.out.println("计算结果是:"+(f1.get()+f2.get()));
//4.关闭线程池
es.shutdown();
}
}
7.3Future接口
- 概念:异步接收ExecutorService.submit()所返回的状态结果,当中包含了call()的返回值。
- 方法:V get()以阻塞形式等待Future中的一部处理结果(call()的返回值)。
7.4同步与异步
7.4.1线程的同步
同步:
- 形容一次方法调用,同步一旦开始,调用者必须等待该方法返回,才能继续。
- 注:单条执行路径
7.4.2线程的异步
异步:
- 形容一次方法调用,异步一旦开始,像是一次消息传递,调用者告知之后立刻返回。二者竞争时间片,并发执行。
- 注:多条执行路径
7.5Lock接口
- JDK5加入,与synchronized比较,显式定义,结构更灵活。
- 提供更多实用性方法,功能更强大、性能更优越。
- 常用方法:
- void lock()//获取锁,如锁被占用,则等待。
- boolean tryLock()//尝试获取锁(成功返回true。失败返回false,不阻塞)。
- void unlock()//释放锁。
7.5.1重入锁
- ReentrantLock:Lock接口的实现类,与sychronized一样具有互斥锁功能。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test {
private Lock locker = new ReentrantLock();//1.创建重入锁对象
private String[] strs = {"A","B","","",""};
private int count = 2;//元素个数
//添加元素
public void add(String value) {
locker.lock();//2.显式开启锁
try {
strs[count] = value;
try {
Thread.sleep(1000);//主动休眠1秒钟
} catch (InterruptedException e) {
// TODO: handle exception
}
} finally {
locker.unlock();//3.显示释放锁
//4.考虑可能出现的异常,释放锁必须放入finally代码块中,避免无法释放。
}
}
}
7.5.2读写锁
- ReentrantReadWriteLock:
- 一种支持一写多读的同步锁,读写分离,可分别分配读锁、写锁。
- 支持多次分配读锁,使多个读操作可以并发执行。
- 互斥规则:
- 写-写:互斥,阻塞。
- 读-写:互斥,读阻塞写、写阻塞读。
- 读读:不互斥、不阻塞。
- 再读操作远远高于写操作的环境中,可在保障线程安全的情况下,提高运行效率。
Class MyClass{
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
ReadLock readLock = rwl.readLock();//获得读锁
WriteLock writeLock = rwl.writeLock;//获得写锁
private int value;
//读方法
public int getValue() throws Exception{
readLock.lock();//开启读锁
try{
Thread.sleep(1000);//休眠1秒
return value;
} finally {
readLock.unlock();//释放读锁
}
}
//写方法
public void setValue(int value) throws Exception {
writeLock.lock();//开启写锁
try{
Thread.sleep(1000);//休眠1秒
this.value = value;
} finally {
writeLock.unlock();//释放写锁
}
}
}
public class TestReadWriteLock {
public static void main(String[] args){
final MyClass mc = new MyClass();
Runnable task1 = new Runnable(){
public void run(){
try{
mc.setValue(1);
} catch(Exception e){
}
}
};
Runnable task2 = new Runnable(){
public void run(){
try{
mc.getValue();
} catch(Exception e){
}
}
};
ExecutorService es = Executors.newFixedThreadPool(20);
long startTime = System.currentTimeMillis();
for(int i = 0;i < 2;i++){
es.submit(task1);//提交2次写任务
}
for(int i = 0;i < 18;i++){
es.submit(task);//提交18次读任务
}
es.shutdown();//关闭线程池
while(!es.isTerminated()){}//如果线程未全部结束,则空转等待
System.out.println(System.currentTimeMillis()-startTime);
}
}
运行结果:互斥锁运行时间20034毫秒、读写锁运行时间3004毫秒。(2次写各占1秒,18次读共占1秒)
7.6线程安全的集合
- Collection体系集合、以及线程安全集合。
注:绿色代表新增知识,下划线代表线程安全集合
7.6.1Collection中的工具方法
- Collections工具类中提供了多个可以获得线程安全集合的方法。
- public static Collection synchronizedCollection(Collection c)
- public static List synchronizedList(List list)
- public static Set synchronizedSet(Set s)
- public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
- public static SortedSet synchronizedSortedSet(SortedSet s)
- public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m)
- JDK1.2提供,接口统一、维护性高,但性能没有提升,均以synchronized实现。
7.6.2CopyOnWriteArrayList
- 线程安全的ArrayList,加强版读写分离。
- 写有锁,读无锁,读写之间不阻塞,优于读写锁。
- 写入时,先copy一个容器副本、再添加新元素,最后替换引用。
- 使用方式与ArrayList无异。
public class TestCopyOnWriteArrayList{
public static void main(String[] args){
List<String> list = new CopyOnWriteArrayList<String>();
}
}
7.6.3ConcurrentHashMap
- 初始容量默认为16段(Segment),使用分段锁设计。
- 不对整个Map加锁,而是为每个Segment加锁。
- 当多个对象存入同一个Segment时,才需要互斥。
- 最理想的状态为16个对象分别存入16个Segment,并行数量16。
- 使用方式与HashMap无异
public class TestConcurrentHashMap{
public static void main(String[] args){
Map<String,String> map = new ConcurrentHashMap<String,String>();
}
}
7.6.4Queue接口(队列)
- Collection的子接口,表示队列FIFO(First In First Out)
- 常用方法:
- 抛出异常:
- boolean add(E e)//顺序添加一个元素(到达上限后,再添加则会抛出异常)
- E remove()//获得第一个元素并移除(如果队列没有元素时,则抛异常)
- E element()//获得第一个元素但不移除(如果队列没有元素时,则抛异常)
- 返回特殊值:推荐使用
- boolean offer(E e)//顺序添加一个元素(到达上限后,再添加则会返回false)
- E poll()//获得第一个元素并移除(如果队列没有元素时,则返回null)
- E peek()//获得第一个元素但不移除(如果队列没有元素时,则返回null)
7.6.5ConcurrentLinkedQueue
- 线程安全、可高效读写的队列,并发下性能最好的队列。
- 无锁、CAS比较交换算法,修改的方法包含三个核心参数(V,E,N)
- V:要更新的变量、E:预期值、N:新值。
- 只有当V==E时,V=N;否则表示已被更新过,则取消当前操作。
public class TestConcurrentLinkedQueue{
public static void main(String[] args){
Queue<String> queue = new ConcurrentLinkedQueue<String>();
queue.offer("Hello");//插入
queue.offer("World");//插入
queue.poll();//删除Hello
queue.peek();//获得World
}
}
7.6.6BlockingQueue接口(阻塞队列)
- Queue的子接口,阻塞的队列,增加了两个线程状态为无限期等待的方法。
- 方法:
- void put(E e)//将指定元素插入此队列中,如果没有可用空间,则等待。
- E take()//获取并移除次队列头部元素,如果没有可用元素,则等待。
- 可用于解决生产者、消费者问题。
7.6.7阻塞队列
ArrayBlockingQueue:
- 数组结构实现,有界队列。(手工固定上限)
public class TestArrayBlockingQueue{ public static void main(String[] args){ BlockingQueue<String> abq = new ArrayBlockingQueue<String>(10); } }
LinkedBlockingQueue:
- 链表结构实现,无界队列。(默认上限Integer.MAX_VALUE)
public class TestLinkedBlockingQueue{ public static void main(String[] args){ BlockingQueue<String> lbq = new LinkedBlockingQueue<String>(); } }
(阻塞队列)
- Queue的子接口,阻塞的队列,增加了两个线程状态为无限期等待的方法。
- 方法:
- void put(E e)//将指定元素插入此队列中,如果没有可用空间,则等待。
- E take()//获取并移除次队列头部元素,如果没有可用元素,则等待。
- 可用于解决生产者、消费者问题。
7.6.7阻塞队列
ArrayBlockingQueue:
- 数组结构实现,有界队列。(手工固定上限)
public class TestArrayBlockingQueue{ public static void main(String[] args){ BlockingQueue<String> abq = new ArrayBlockingQueue<String>(10); } }
LinkedBlockingQueue:
- 链表结构实现,无界队列。(默认上限Integer.MAX_VALUE)
public class TestLinkedBlockingQueue{ public static void main(String[] args){ BlockingQueue<String> lbq = new LinkedBlockingQueue<String>(); } }