目录
1.概念理解
1.1并发和并行
- 并发:在同一时刻,有多个指令在单个CPU上交替执行。
- 并行:在同一时刻,有多个指令在多个CPU上同时执行。
1.2进程和线程
- 进程:是正在运行的程序
- 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
- 动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的
- 并发性:任何进程都可以同其他进程一起并发执行
- 线程:是进程中的单个顺序控制流,是一条执行路径
- 单线程:一个进程如果只有一条执行路径,则称为单线程程序
- 多线程:一个进程如果有多条执行路径,则称为多线程程序
2.实现多线程方式
2.1方法一:继承Thread类
class MyThread extends Thread {
@Override
public void run() {
for(int i=0; i<100; i++) {
System.out.println(getName()+":"+i);
}
}
}
public class MyThreadDemo {
public static void main(String[] args) {
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
//start() 导致此线程开始执行; Java虚拟机调用此线程的run方法
my1.start();
my2.start();
}
}
如果直接调用run() ,相当于普通方法的调用,并不会启动线程。只有调用 start() 方法才会启动线程,然后由JVM调用此线程的run()方法
2.2方法二:实现Runnable接口
Thread类也是实现了Runnable接口才能实现多线程,所以我们可以直接实现Runnable接口:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
class MyRunnable implements Runnable {
@Override
public void run() {
for(int i=0; i<100; i++) {
//Thread.currentThread().getName() 获取当前线程的名字
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
public class MyRunnableDemo {
public static void main(String[] args) {
//创建MyRunnable类的对象
MyRunnable my = new MyRunnable();
//创建Thread类的对象,把MyRunnable对象作为构造方法的参数
//Thread(Runnable target)
//Thread(Runnable target, String name)
Thread t1 = new Thread(my,"坦克");
Thread t2 = new Thread(my,"飞机");
//启动线程
t1.start();
t2.start();
}
}
2.3方法三:实现Callable接口
可以获取多线程运行的结果,即有返回值。需要先启动线程后再 get
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println("跟女孩表白" + i);
}
//返回值就表示线程运行完毕之后的结果
return "答应";
}
}
public class Demo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//线程开启之后需要执行里面的call方法
MyCallable mc = new MyCallable();
//可以获取线程执行完毕之后的结果.也可以作为参数传递给Thread对象
FutureTask<String> ft = new FutureTask<>(mc);
//创建线程对象
Thread t1 = new Thread(ft);
//开启线程
t1.start();
String s = ft.get();
System.out.println(s);
}
}
3.Thread类中的常用成员方法
3.1设置和获取线程名称
方法名 | 说明 |
---|---|
void setName(String name) | 将此线程的名称更改为等于参数name |
String getName() | 返回此线程的名称 |
Thread currentThread() | 返回对当前正在执行的线程对象的引用 |
class MyThread extends Thread {
public MyThread() {}
public MyThread(String name) {
//调用父类构造器
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+":"+i);
}
}
}
public class MyThreadDemo {
public static void main(String[] args) {
// MyThread my1 = new MyThread();
// MyThread my2 = new MyThread();
// my1.setName("高铁");
// my2.setName("飞机");
//Thread(String name)
MyThread my1 = new MyThread("高铁");
MyThread my2 = new MyThread("飞机");
my1.start();
my2.start();
//static Thread currentThread() 返回对当前正在执行的线程对象的引用
System.out.println(Thread.currentThread().getName());
}
}
3.2线程休眠
方法名 | 说明 |
---|---|
static void sleep(long millis) | 使当前正在执行的线程停留(暂停执行)指定的毫秒数 |
3.3线程优先级
线程调度的两种方式
- 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
- 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
Java使用的是抢占式调度模型,优先级越高,抢到CPU的概率更大一些
方法名 | 说明 |
---|---|
final int getPriority() | 返回此线程的优先级 |
final void setPriority(int newPriority) | 更改此线程的优先级线程默认优先级是5;线程优先级的范围是:1-10 |
class MyCallable implements Callable<String> {
@Override
public String call(){
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "---" + i);
}
return "线程执行完毕了";
}
}
public class Demo {
public static void main(String[] args) {
//优先级: 1 - 10 默认值:5
MyCallable mc = new MyCallable();
FutureTask<String> ft = new FutureTask<>(mc);
Thread t1 = new Thread(ft);
t1.setName("飞机");
t1.setPriority(10);
t1.start();
MyCallable mc2 = new MyCallable();
FutureTask<String> ft2 = new FutureTask<>(mc2);
Thread t2 = new Thread(ft2);
t2.setName("坦克");
t2.setPriority(1);
t2.start();
}
}
3.4守护线程
当用户线程(非守护线程)执行完毕后,守护线程会陆续结束(即便守护线程内的任务还没有执行完也会结束)
方法名 | 说明 |
---|---|
void setDaemon(boolean on) | 将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出 |
class MyThread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName() + "---" + i);
}
}
}
class MyThread2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "---" + i);
}
}
}
public class Demo {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
MyThread2 t2 = new MyThread2();
t1.setName("女神");
t2.setName("备胎");
//把第二个线程设置为守护线程
//当普通线程执行完之后,那么守护线程也没有继续运行下去的必要了.
t2.setDaemon(true);
t1.start();
t2.start();
}
}
可以看到当非守护线程执行完毕后,即使守护线程内的任务未执行完也会陆续结束
4.线程同步
线程不安全:因为没有采用加锁机制,不提供数据访问保护,当多线程访问共享资源时,有可能出现多个线程先后更改数据造成所得到的数据是脏数据
经典的超卖+重复卖票问题:
class SellTicket implements Runnable {
private static int ticket = 100;
//在SellTicket类中重写run()方法实现卖票,代码步骤如下
@Override
public void run() {
while (true) {
if(ticket <= 0){
//卖完了
break;
}else{
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
}
}
}
}
public class SellTicketDemo {
public static void main(String[] args) {
//创建SellTicket类的对象
SellTicket st = new SellTicket();
//创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称
Thread t1 = new Thread(st,"窗口1");
Thread t2 = new Thread(st,"窗口2");
Thread t3 = new Thread(st,"窗口3");
//启动线程
t1.start();
t2.start();
t3.start();
}
}
线程同步
解决线程并发问题的方法是线程同步,线程同步就是让线程排队,就是操作共享资源要有先后顺序,一个线程操作完之后,另一个线程才能操作或者读取。如果多线程访问同一份可变的共享资源,那么这些线程之间就需要同步,需要同步的话就需要加锁
synchronized锁
synchronized 锁默认打开,有一个线程进去了,锁会自动关闭;等到里面的代码全部执行完毕,线程出来,锁会自动打开。
同步代码块
格式:
synchronized(唯一的对象) {
//多条语句操作共享数据的代码
}
使用同步代码块来解决超卖+重复卖票问题,注意循环应该在同步代码块的外面,只锁住多线程操作共享资源的片段
class SellTicket implements Runnable {
private static int ticket = 10;
//在SellTicket类中重写run()方法实现卖票,代码步骤如下
@Override
public void run() {
while (true) {
//针对多线程来说的唯一对象,代表着针对每个线程都是同一把锁
synchronized (SellTicket.class){
if(ticket <= 0){
//卖完了
break;
}else{
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
}
}
}
}
}
public class SellTicketDemo {
public static void main(String[] args) {
//创建SellTicket类的对象
SellTicket st = new SellTicket();
//创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称
Thread t1 = new Thread(st,"窗口1");
Thread t2 = new Thread(st,"窗口2");
Thread t3 = new Thread(st,"窗口3");
//启动线程
t1.start();
t2.start();
t3.start();
}
}
锁对象要唯一并且共享(非局部变量),每个线程都有机会能抢到
这里的锁对象是 类名.class,即字节码文件,这是唯一并且共享的。我们需要保证各个线程需要抢夺的是同一把锁,如果是每个线程进入代码块需要的锁都不一样,那么这加锁完全无用,等同于不加。并且锁对象需要是共享的,否则也毫无意义。
如下图,如果锁对象不唯一,则和不加锁毫无区别,线程访问共享资源照样畅通无阻
同步方法
方法内的所有内容都会被锁住,所以尽量保证方法里的都是 对共享资源的操作,否则锁的太多非常影响效率。为了确定范围,通常可以先写同步代码块,再改成同步方法。
同步方法的锁对象不能自己指定。
- 如果是非静态同步方法,锁对象则为this,即调用当前方法的对象
- 如果是静态同步方法,锁对象则为当前类的字节码文件对象
使用同步方法来解决超卖+重复卖票问题,如下:
class SellTicket implements Runnable {
private static int ticket = 10;
//在SellTicket类中重写run()方法实现卖票,代码步骤如下
@Override
public void run() {
while (true) {
if (sell()) break;
}
}
public synchronized boolean sell() {
if (ticket <= 0) {
//卖完了
return true;
} else {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
return false;
}
}
}
public class SellTicketDemo {
public static void main(String[] args) {
//创建SellTicket类的对象
SellTicket st = new SellTicket();
//创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");
//启动线程
t1.start();
t2.start();
t3.start();
}
}
lock锁
synchronized 锁并没有显式的加锁操作和释放锁操作,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化。注意:还是需要保证锁对象的唯一性和共享性
Lock中提供了获得锁和释放锁的方法
- void lock():获得锁
- void unlock():释放锁
使用 lock锁 来解决超卖+重复卖票问题,如下:
class SellTicket implements Runnable {
private static int ticket = 10;
//static保证锁对象唯一且共享
private static Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
//针对多线程来说的唯一对象,代表着针对每个线程都是同一把锁
lock.lock();
try {
if (ticket <= 0) {
//卖完了
break;
} else {
Thread.sleep(10);
ticket--;
System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//建议在finally统一释放锁
lock.unlock();
}
}
}
}
public class SellTicketDemo {
public static void main(String[] args) {
//创建SellTicket类的对象
SellTicket st = new SellTicket();
//创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");
//启动线程
t1.start();
t2.start();
t3.start();
}
}
5.多线程通信(等待唤醒机制)
线程协作指不同线程驱动的任务相互依赖,依赖一般就是对共享资源的依赖。(有共享就有竞争,有竞争就会有线程安全问题(即并发),解决并发问题就用线程同步)。
应用场景:生产者和消费者问题
- 假如桌子上只能存放一碗面条,生产者将生产出来的面条放到桌子上,消费者将桌子上的面条取走消费。
- 如果桌子上没有面条,则生产者生产面条放到桌子上,否则停止生产并等待,直到桌子上的面条被消费者取走为止。
- 如果桌子上有面条,则消费者可以将面条取走消费,否则停止消费并等待,直到桌子上再次放入面条为止。
场景分析:这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。
在生产者消费者问题中,没生产出产品之前,消费者是不能消费的,反之,消费者没消费完之前,生产者是不能生产的。这就需要锁来实现线程之间的同步。仅有同步还不行,还要实现线程之间的消息传递,即通信。
在Object类中提供了wait (), notify (), notifyAll ()方法用于解决线程间的通信问题,由于Java中所有类都是Object类的子类或间接子类,因此任何类的实例对象都可以直接使用这些方法。接下来通过详细说明这几个方法的作用。
因为 notify () 是 唤醒一个,所以通常会用 notifyAll ()。当然不可能唤醒操作系统的所有线程,所以这些方法需要和锁对象绑定,而非直接使用。
- notify:唤醒队列中第一个等待线程(等待时间最长的线程),使其从wait()方法返回,而返回的前提时该线程获取到对象的锁。
- notifyAll:通知所有等待在该对象上的线程。notify()/notifyAll() 只能唤醒等待在同一把锁上的线程。
- wait:调用此方法的线程进入阻塞等待状态,并且会被加入到一个等待队列,只有等待另外线程的通知或者被中断才会返回,调用wait方法会释放对象的锁
注意:均是Object的方法,均只能在同步方法或者同步代码块中使用,否则会抛出异常IIIegalMonitorStageException。
场景实现如下:
Desk 类(缓存区)
public class Desk {
/*
* 作用:控制生产者和消费者的执行
*
* */
//是否有面条 0:没有面条 1:有面条
public static int foodFlag = 0;
//总个数
public static int count = 10;
//锁对象
public final static Object lock = new Object();
}
Cook类(生产者)
public class Cook extends Thread{
@Override
public void run() {
while (true){
synchronized (Desk.lock){
if(Desk.count == 0){
break;
}else{
//判断桌子上是否有食物
if(Desk.foodFlag == 1){
//如果有,就等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
//如果没有,就制作食物
System.out.println("厨师做了一碗面条");
//修改桌子上的食物状态
Desk.foodFlag = 1;
//叫醒等待的消费者开吃
Desk.lock.notifyAll();
}
}
}
}
}
}
Comsumer类(消费者)
public class Comsumer extends Thread{
@Override
public void run() {
while(true){
synchronized (Desk.lock){
if(Desk.count == 0){
break;
}else{
//先判断桌子上是否有面条
if(Desk.foodFlag == 0){
//如果没有,就等待
try {
Desk.lock.wait();//让当前线程跟锁进行绑定
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
//把吃的总数-1
Desk.count--;
//如果有,就开吃
System.out.println("吃货在吃面条,还能再吃" + Desk.count + "碗!!!");
//吃完之后,唤醒厨师继续做
Desk.lock.notifyAll();
//修改桌子的状态
Desk.foodFlag = 0;
}
}
}
}
}
}
ThreadDemo测试类
public class ThreadDemo {
public static void main(String[] args) {
//创建线程的对象
Cook c = new Cook();
Comsumer f = new Comsumer();
//给线程设置名字
c.setName("厨师");
f.setName("吃货");
//开启线程
c.start();
f.start();
}
}
使用阻塞队列实现
生产者和消费者 必须使用同一个阻塞队列。这里我使用ArrayBlockingQueue,实现如下:
Cook类(生产者)
public class Cook extends Thread{
private ArrayBlockingQueue<String> queue;
public Cook(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while(true){
//不断的把面条放到阻塞队列当中
try {
//底层加了锁
queue.put("面条");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Comsumer类(消费者)
public class Comsumer extends Thread{
private ArrayBlockingQueue<String> queue;
public Comsumer(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while(true){
//不断从阻塞队列中获取面条
try {
//底层加了锁
String food = queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
ThreadDemo测试类
public class ThreadDemo {
public static void main(String[] args) {
//1.创建阻塞队列的对象
//指定容量
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
//2.创建线程的对象,并把阻塞队列传递过去
Cook c = new Cook(queue);
Comsumer f = new Comsumer(queue);
//3.开启线程
c.start();
f.start();
}
}
ArrayBlockingQueue阻塞队在列底层加了锁,所以无需再加
6.线程的生命周期
事实上,在Java虚拟机中,只定义了 6种 线程状态,并没有定义运行状态。当线程在就绪状态抢到CPU的执行权后,此时虚拟机会把线程交给操作系统去管理
下面才是Java虚拟机中定义的线程状态
- 新建状态(New):新创建了一个线程对象。
- 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
- 阻塞状态(Blocked):运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中,进入阻塞状态。
- 等待状态(Waiting):运行的线程执行wait()方法,JVM会把该线程放入等待池中,进入等待状态。(wait会释放持有的锁)
- 计时等待状态(Timed_Waiting):运行的线程执行sleep()方法后,进入计时等待状态。(sleep不会释放持有的锁)
- 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
7.线程池
线程池就是首先创建一些线程,他们的集合称之为线程池。线程池在系统启动时会创建大量空闲线程,程序将一个任务传递给线程池,线程池就会启动一条线程来执行这个任务,执行结束后线程不会销毁(死亡),而是再次返回到线程池中成为空闲状态,等待执行下一个任务。
通过JDK自带的工具类创建线程池
JDK对线程池也进行了相关的实现,我们可以使用 Executors 中所提供的静态方法来创建线程池
- public static ExecutorService newCachedThreadPool() 创建一个默认的线程池
- public static ExecutorService newFixedThreadPool(int nThreads) 创建一个指定最多线程数量的线程池
如下:
//1.获取线程池对象
ExecutorService pool1 = Executors.newFixedThreadPool(10);
//2.提交任务
pool1.submit(new MyRunnable());
自定义线程池
线程池的真正实现类是 ThreadPoolExecutor,其构造方法最多的有7个参数
- 核心线程数(必需):默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
int processors = Runtime.getRuntime().availableProcessors();
- 线程池所能容纳的最大线程数(必需):最大线程数=核心线程数+临时线程数。
processors + processors >> 1
- 线程闲置超时时长(必需):如果超过该时长,非核心线程(临时线程)就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
- 线程闲置超时时长的时间单位(必需):常用的有:TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
- 任务队列(必需):通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
- 线程工厂(可选):用于指定为线程池创建新线程的方式。线程工厂指定创建线程的方式,需要实现 ThreadFactory 接口,并实现 newThread(Runnable r) 方法。该参数可以不用指定,Executors 框架已经为我们实现了一个默认的线程工厂:Executors.defaultThreadFactory()
- 拒绝策略(可选):当达到最大线程数时需要执行的饱和策略。
拒绝策略
线程池的工作原理
当核心线程都处于工作状态情况下,如再有任务,会先填满任务队列,再创建临时线程
自定义线程池代码如下:
public class MyThreadPool {
public static void main(String[] args){
/*
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor
(核心线程数量,最大线程数量,空闲线程最大存活时间,任务队列,创建线程工厂,任务的拒绝策略);
参数一:核心线程数量 不能小于0
参数二:最大线程数 不能小于0,最大数量 >= 核心线程数量
参数三:空闲线程最大存活时间 不能小于0
参数四:时间单位 用TimeUnit指定
参数五:任务队列 不能为null
参数六:创建线程工厂 不能为null
参数七:任务的拒绝策略 不能为null
*/
ThreadPoolExecutor pool = new ThreadPoolExecutor(
10, //核心线程数量,能小于0
20, //最大线程数,不能小于0,最大数量 >= 核心线程数量
60,//空闲线程最大存活时间
TimeUnit.SECONDS,//时间单位
new ArrayBlockingQueue<>(5),//任务队列
Executors.defaultThreadFactory(),//创建线程工厂
new ThreadPoolExecutor.AbortPolicy()//任务的拒绝策略
);
}
}
不用的时候记得关闭线程池,threadPool.shutdown();
最大线程数设置
- I/O密集型(即操作数据库、文件或者调用 RPC等居多):可设置 2n 个最大线程;
- CPU密集型(计算居多):可设置 n+1 个最大线程。n 为服务器的逻辑处理器个数。