多线程编程(Thread–多线程)
进程:正在进行的程序。
线程:指的是 进程当中某一个子任务或者子功能。
线程作用
- 解决阻塞,提高程序的运行效率,充分利用硬件资源。
并发与并行
并发 | 并行 |
---|---|
并发是指两个或多个事件在同一时间间隔发生 | 并行是指两个或者多个事件在同一时刻发生 |
并发是在同一实体上的多个事件 | 并行是在不同实体上的多个事件 |
发是在多台处理器上同时处理多个任务 | 并行是在一台处理器上“同时”处理多个任务 |
更多内容参考: https://www.jianshu.com/p/cbf9588b2afb
线程的两种创建方式
- 继承Thread类 重写run()方法
package com.qianfeng.thread;
public class MyThread extends Thread{
public void run(){
for (int i = 0; i < 1000; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
- 实现Runnable接口 实现run() 方法
package com.qianfeng.thread;
public class MyThread1 implements Runnable{
public void run() {
for (char i = 'a'; i < 'z'; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
开启线程的方法
- 继承方式的 通过调用自定义线程类的start()方法 开启一个线程
MyThread t1=new MyThread();
MyThread t2=new MyThread();
t1.start();
t2.start();
- 实现方式的
@Test
public void test() {
MyThread1 m1=new MyThread1();
Thread t1=new Thread(m1);
Thread t2=new Thread(m1);
t1.start();
t2.start();
}
- 注意
- 启动线程类的方法是start() ,使线程进入就绪状态
- 启动之后不是直接运行run方法 而是等待cpu分配时间片 当cpu分配了时间片之后 就会运行run方法。
线程的基本状态
四个基本状态
-
初始状态----->相当于是新创建了线程对象 Thread t1=new Thread(task);
-
就绪状态------>调用了线程类的start方法。 t1.start();注:cpu还没有分配时间片
-
运行状态------>cpu分配了时间片给当前线程 run方法被执行 。 run()…
-
结束状态------>当线程 跑完run方法之后。
线程的常用方法
方法 | 含义 |
---|---|
Thread.currentThread().getName(); | 获得当前线程的名称 |
MyTread t1=new Mythread(); t1.setName(“线程一”); | 给某个线程设置名称 |
t1.isDaemon(); | 判断当前线程是否是守护线程(类型于gc) |
t1.getPriority(); | 获得当前线程的优先级 |
t1.setPriority(); | 设置当前线程的优先级 |
Thread.sleep(long millis) | 当前的线程 等待 一个毫秒值 |
t1.join(); | 在当前的线程中 强势加入另一个线程 需要使加入进来的这线程 先运行完 我们的当前线程才可以继续运行。 |
Thread.currentThread().yield(); | 当前线程让出cpu时间片 重新使当前线程处于就绪状态 , 再次获得时间片时,继续执行礼让后的代码 |
线程的优先级
- 默认的线程优先级是 5 main方法的优先级也是5
- 一共有10 1-10 数字越大 优先级越高 约容易被cpu调度
- 注意了!不绝对 就先调用优先级高的!有很小的概率也可能会调度到优先级的线程。
线程常用的方法案例–sleep,join,yield
-
当前的线程 等待 一个毫秒值
Thread.sleep(long millis)
-
在当前的线程中 强势加入另一个线程
- 需要使加入进来的这线程 先运行完 我们的当前线程才可以继续运行。
public static void main(String[] args) throws InterruptedException { MyThread t1=new MyThread(); t1.start(); MyThread t2=new MyThread(); t2.start(); for (int i = 0; i < 1000; i++) { if(i==50){ t1.join(); } System.out.println("main:"+i); } }
-
线程礼让方法 yield();
- 当前线程让出cpu时间片 重新使当前线程处于就绪状态 ,再次获得时间片时,继续执行礼让后的代码
- 相当于是所有的线程重新回到了竞争的状态 有可能当前线程继续获得到cpu时间片 也有可能是别的线程获得到cpu时间片。不确定性。
@Test public void testYield(){ new Thread(new Runnable() { @Override public void run() { Thread.currentThread().yield(); for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName()+":"+i); } } }).start(); new Thread(new Runnable() { @Override public void run() { Thread.currentThread().yield(); for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName()+":"+i); } } }).start(); }
- 当前线程让出cpu时间片 重新使当前线程处于就绪状态 ,再次获得时间片时,继续执行礼让后的代码
等待状态
阻塞状态
线程状态(6/7)
-
JDK5之后,把线程的状态归纳为6个状态,其中就绪状态和运行状态都属性 启动状态。
-
线程状态:7种(jdk1.5之前)
-
初始状态:new Thread();
-
就绪状态:调用start方法
-
运行状态:获取时间片
-
有限期等待:调用Thread.sleep(毫秒值),等待结束之后,进入就绪状态,等待时间片的分配
-
无限期等待:在当前线程中,调用其它线程的join方法,等其它线程执行完毕之后,当前线程进入就绪状态,等待时间片的分配
-
阻塞状态:当线程去获取对象的锁对象时,如果此时对象上没有锁标记,则该线程就会阻塞。
-
结束状态:线程执行完毕
-
-
-
这些状态定义在枚举Thread.State中:
线程安全问题
-
产生的原因:
- 多个线程同时操作1个对象,这个对象就是临界资源(共享资源),必须保证一些连续的操作全部执行完毕,才能保证线程是安全的。
-
多个密不可分的操作称为“原子操作”,即不可再分!
在上述的例子中,两个线程都可以访问的那个数组,就是临界资源,判断是否为null位和将值存入到指定的位置,就是一个“原子操作”
- 如果没法保证“原子操作”,就无法保证线程安全,也就无法保证数据的完整性!
线程同步
临界资源: 多个线程同时操作1个对象,这个对象就是临界资源(共享资源)
方式1:同步代码块
//临界资源最好为类对象(例this),如果换成非引用对象(eg:Integer对象),有时会失效
synchronized ( 临界资源 ) {
// 原子操作
}
- 原理
方式2:同步方法
同步方法:临界资源是this
public synchronized void xxx() {
// 原子操作
}
- 何时释放锁:线程运行完毕之后,释放临界资源的锁。
线程死锁
- 死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象
线程通信–wait() ,notify(), notifyAll()
-
wait() ,notify(), notifyAll()都源自Object–所有的类都有这三个方法
-
wait()
- 会使当前线程进入等待状态
- 并且会释放当前线程拥有得互斥锁标记
- (如果wait()方法调用得时候 没有锁标记 则会抛出异常)
-
notify();
- 会唤醒一个等待的线程
-
notifyAll();
- 会唤醒所有的等待的线程
-
notify和notifyAll() 注意点
- 被唤醒的线程 唤醒之后 处于一个就绪状态 也就是说 不会立马被执行
- 被唤醒的线程 并不会持有对象锁 而是有资格去竞争
- 执行了唤醒操作 当前的线程并不会立即交出锁标记
生产者和消费者案例–注意假唤醒–if(条件变量与量冲突)
- 说明
- 用while代替if防止出现假唤醒的情况
- 解决了刚唤醒后出现超出临界情况的现象
- 循环判断,只要被唤醒,再次执行循环条件,判断是否超出临界条件
- 唤醒前因超出临界条件被挂起线程进入等待状态,唤醒后因其他线程抢占资源而依旧超出临界条件
- 线程传值–用构造方法
- 线程同步–临界资源最好为类对象(例this),如果换成其他对象(eg:Integer对象),有时会失效
package com.qianfeng.Thread;
import java.util.*;
public class KFC {//实体列--KFC
private LinkedList<String> items=new LinkedList<String>();
public synchronized void put() throws InterruptedException {
while(items.size()>=5) {,
this.wait();
}
items.add("汉堡");
System.out.println(Thread.currentThread().getName()+":生产一个汉堡,剩余:"+items.size());
this.notifyAll();
}
public synchronized void get() throws InterruptedException {
while(items.size()==0) {
this.wait();
}
items.remove(0);
System.out.println(Thread.currentThread().getName()+":消费一个汉堡剩余:"+items.size());
this.notifyAll();
}
}
//生产者
package com.qianfeng.Thread;
public class Produce implements Runnable{
private KFC kfc;
public Produce(KFC kfc) {
this.kfc = kfc;
}
@Override
public void run() {
while(true) {
try {
kfc.put();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
//消费者
package com.qianfeng.Thread;
public class Consume implements Runnable{
private KFC kfc;
public Consume(KFC kfc) {
this.kfc = kfc;
}
@Override
public void run() {
while(true) {
try {
kfc.get();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
//测试Demo
package com.qianfeng.Thread;
public class Demo {
public static void main(String[] args) {
KFC kfc=new KFC();//创建KFC类对象
Produce p=new Produce(kfc);//创建生产者线程,赋予同一个类属性方法
Consume c=new Consume(kfc);//创建消费者线程,赋予同一个类属性方法
Thread t1=new Thread(p,"生产者1");
Thread t2=new Thread(c,"消费者1");
Thread t3=new Thread(p,"生产者2");
Thread t4=new Thread(c,"消费者2");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
线程池
- 背景(常见问题)
- 线程是宝贵的内存资源、单个线程约占1MB空间,,过多的分配易造成内存溢出
- 频繁的创建及销毁线程会证件虚拟机的回收频率,资源开销,降低程序性能
- 概念
- 线程容器,可设定线程的分配的数量上限
- 将预先创建的线程对象存入池中,并重用线程池中 线程对象
- 避免频繁的创建和销毁,提高程序的响应速度
- 原理
线程池的使用方法
-
创建线程池–Executor:线程池的顶级接口。
- Executors.newFixedThreadPool(int n)//创建固定大小的线程池
- Executors.newCachedThreadPool();//创建动态线程池案例
-
创建案例
// 创建线程的个数需要根据系统规模及内存作为参考 ExecutorService pool1 = Executors.newFixedThreadPool(5); // 该方式创建的线程池中的线程数是动态维护的:不够则创建,空闲则销毁,不适用于访问量激增的程序。 ExecutorService pool2 = Executors.newCachedThreadPool();
-
线程池–创建任务,开启任务
- 实现Runnable接口==》线程的任务 ==>无返回值,
- 实现Callable接口==》线程的任务 ==>有返回值,返回值存放在Future<返回值类型>中
- 实现的方法需抛出异常
- Future 线程池对象.submit()方法的返回值类型
- 可以通过get方法获得 返回值
- get方法是同步方法
- 会等真正产生返回值再去运行
- ExecutorService 的对象es
- es.submit(上述的实现接口);
-
接口实现案例–匿名内部类
ExecutorService es = Executors.newCachedThreadPool();//这是一个动态的线程池 //Runnable es.submit(new Runnable() {//Rannable @Override public void run() { for (int i = 0; i < 1000; i++) { System.out.println(i); } } }); //Callable Future <T> f =es.submit(new Callable<T>() {//Callable @Override public T call()throws Exception { int sum=0; for (int i = 0; i < 1000; i++) sum+=i; return sum;//决定了了泛型的类型--T :Integer } }); //获取值 //f.get()-主线程调用时会等线程真正产生返回值后再去运行
Lock ReentrantLock(重入锁)–类似于synchronize,但有不同
多线程使用synchronized来保持线程之间同步互斥,jdk1.5中加入了Lock对象也能实现同步效果
Re entra nt Lock(rɪ’entrənt)类的使用,ReentrantReadWriteLock类的使用
-
ReentrantLock 概述
- java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。
-
ReentrantLock与synchonized的异同
- 同:它拥有与 synchronized 相同的并发性和内存语义
- 异: ReentrantLock 类实现了 Lock ,但是添加了类似轮询锁、定时锁等候和可中断锁等候的一些特性。
-
优势
- 这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。
- 它还提供了在激烈争用情况下更佳的性能。
- 换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上
-
reentrant 锁意味着什么呢?
简单来说,它有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。这模仿了 synchronized 的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续) synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个 synchronized 块时,才释放锁。
ReentrantLock 的使用–lock与unlock,await,signal
-
lock与unlock
- lock.lock()+lock.unlock();包起来的代码 == synchronized包起来的代码。
- lock.unlock最好还是放在finally中,这样才能安全的解锁,不然lock住的线程会一直阻塞。
- 同一套lock与unlock配对,如果其中lock执行完抛出异常,
- 如果无法执行对应的unlock,lock住的线程会一致阻塞
-
await(),signal() 线程通信–类似于wait(),notify()/notifyAll()
- 一个lock可以有多个condition(lock 可以拥有多个Condition)
- 每个Condition可以独自拥有自己的await,signal。
-
案例代码–与sychronized+wait()+notify()的一致的代码
package cn.thread.lock.reentrant; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class MyService { private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public void serviceA() { try { //Thread.sleep(100); lock.lock();// lock是一把锁 code..... condition.await(); //线程挂起,进入等待状态 System.out.println(Thread.currentThread().getName() + "----Ai=" + i); } catch (InterruptedException e) { e.printStackTrace(); } finally{ lock.unlock(); //condition.signal();在其他的方法中使用该段代码,唤醒被await挂起的servuceA线程 } }
公平锁与非公平锁–Lock lock [new ReentrantLock(true)]
-
概述
Lock锁分为公平锁和非公平锁
-
公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得FIFO。
- private Lock lock = new ReentrantLock(true);公平锁。
-
非公平锁就是一种获取锁的抢占机制,随机获得锁。
- private Lock lock = new ReentrantLock(); 默认是非公平锁