1 相关概念
并发:两个或多个事件在同一时间段内发生【多个任务交替执行】
并行:两个或多个事件在同一时刻发生【多个任务同时执行】
进程:进入内存的程序
内存:所有应用程序都要进入到内存中执行 临时存储RAM
线程:进程的一个执行单元,负责程序的执行
一个程序至少有一个进程,一个进程可以包含多个线程
CPU:中央处理器,对数据进行计算,指挥软件和硬件
单线程:CPU在多个线程之间做高速的切换,轮流执行多个线程,效率低
多线程:多个线程在多个任务之间做高速的切换,速度是单线程的多倍,多个线程之间互不影响
线程调度:
- 分时调度:所有线程轮流使用CPU,平均分配每个线程占用CPU的时间
- 抢占式调度:优先让优先级高的线程使用CPU,如果优先级相同,则随机选择一个,Java中使用抢占式调度。
2 主线程
主线程:执行main方法的线程
主线程的过程:JVM执行main方法,main方法进入到栈内存,JVM会找操作系统开辟一条main方法的执行路径,CPU根据路径来执行main方法,这个路径就是主线程。
单线程:执行从main方法开始自上而下依次执行
public class Person {private String name;public Person(String name) {this.name = name;}public void run(){for (int i = 0; i < 3; i++) {System.out.println(name + " " + i);}}
}
public class MainThread {public static void main(String[] args) {Person p1 = new Person("张三");p1.run();Person p2 = new Person("李四");p2.run();}
}
3 创建多线程程序-法1
3.1 创建Thread类的子类
- 创建Thread类的子类
- 在子类中重写run方法,设置线程任务
- 创建子类对象
- 调用start方法,开始新的线程,执行run方法
main压栈执行后,在堆内存中创建线程子类对象,栈中保存对象地址。如果调用run方法,run方法压栈执行,则是单线程处理。如果调用start方法,会开辟新的栈空间执行run方法。CPU可以选择不同的栈空间。
start使线程开始执行,JVM调用线程的run方法,两个线程并发运行
(main线程)<----->(创建新线程执行run)
public class MyThread extends Thread {@Overridepublic void run() {for (int i = 0; i < 5; i++) {System.out.println("run " + i);}}
}
public class MyThreadTest {public static void main(String[] args) {MyThread mt = new MyThread();mt.start();for (int i = 0; i < 5; i++) {System.out.println("main " + i);}}
}
同优先级下,随机抢占,谁抢到谁执行
3.2 Thread类常用方法
获取线程名称:getName()、Tread.currentTread()
Tread t = new Tread();
sout(t.getName());//名称
Tread t = Tread.currentTread();
sout(t);//名称
设置线程名称:setName()、构造方法参数传递线程名称
public class MyThreadTest {public static void main(String[] args) {MyThread mt1 = new MyThread("张三");mt1.start();MyThread mt2 = new MyThread();mt2.setName("李四");mt2.start();}
}
public class MyThread extends Thread {public MyThread(String name) {super(name);public MyThread() {}@Overridepublic void run() {System.out.println(getName());}
}
线程休眠:sleep(long millis) 毫秒结束后程序继续执行
模拟秒表 示例:
public class MyThreadTest {public static void main(String[] args) throws InterruptedException {for (int i = 1; i <= 60; i++) {System.out.println(i);Thread.sleep(1000);}}
}
4 创建多线程程序-法2【推荐使用】
4.1 创建Runnable实现类
- 创建Runanable接口的实现类
- 实现类中重写run方法,设置线程任务
- 创建实现类的对象
- 创建Thread类对象,构造方法中传递Runnale的实现类对象
- 调用Thread类中的start方法
public class RunnableImpl implements Runnable {@Overridepublic void run() {for (int i = 0; i < 5; i++) {System.out.println(Thread.currentThread()+" "+ i);}}
}
public class MyThreadTest {public static void main(String[] args) throws InterruptedException {RunnableImpl run = new RunnableImpl();Thread t = new Thread(run);t.start();for (int i = 0; i < 5; i++) {System.out.println(Thread.currentThread() + " " + i);}}
}
4.2 两种实现方法的区别
Runnable的优点:
- 避免了单继承的局限性,类继承了Thread类就不能继承其他类了,实现Runnable接口还可以实现其他接口。
- 增强了程序的扩展性,降低了程序的耦合性(解耦)。把设置线程任务和开启线程进行了分离。实现类中重写run方法来设置线程任务,创建Thread类对象调用start来开启新线程。想要什么任务,就传递什么实现类对象。
5 匿名内部类创建线程
匿名内部类:简化代码。
把1.子类继承父类 2.重写父类 3.创建子类对象 —> 一步完成
把1.实现实现类接口 2.重写接口方法 3.创建实现类对象 —> 一步完成
public class MyThreadTest {public static void main(String[] args) throws InterruptedException {//Thread法new Thread(){@Overridepublic void run() {for (int i = 0; i < 5; i++) {System.out.println(Thread.currentThread() + " " + i);}}}.start();//Runnable法new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 5; i++) {System.out.println(Thread.currentThread() + " " + i);}}}).start();}
}
6 线程安全
共享资源产生安全问题
public class RunnableImpl implements Runnable {//共享票源private int tickets = 100;@Overridepublic void run() {//重复卖票while(true){if(tickets > 0){try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("卖第"+tickets+"张票");tickets--;}}}
}
public class MyThreadTest {public static void main(String[] args) throws InterruptedException {RunnableImpl r = new RunnableImpl();Thread t1 = new Thread(r);Thread t2 = new Thread(r);Thread t3 = new Thread(r);t1.start();t2.start();t3.start();}
}
出现了重复的票
窗口Thread-2在卖第100张票
窗口Thread-0在卖第100张票
窗口Thread-1在卖第100张票
窗口Thread-1在卖第97张票
窗口Thread-2在卖第97张票
窗口Thread-0在卖第97张票
窗口Thread-0在卖第94张票
窗口Thread-2在卖第94张票
出现了不存在的票
窗口Thread-2在卖第0张票
窗口Thread-1在卖第-1张票
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
注意:访问共享数据的时候,无论是否时区CPU执行权,其他线程只能等待,等当前线程完全结束后,其他线程再继续。
7 同步技术的原理
使用了一个锁对象,这个对象叫同步锁,也叫对象监视器。多个线程一起抢夺CPU执行权,谁抢到了,谁执行run方法,遇到同步代码块。
此时,抢到CPU的当前线程T0会检查同步代码块是否有锁对象,如果有,则获取锁对象,进入到同步中进行。
另一进程T1抢到CPU后发现没有锁对象了,则进入阻塞状态,等待锁对象的归还,直到上一进程T0执行完同步代码块才归还锁对象,T1进程才可以获取到锁对象,进入到同步中执行。
同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁对象,无法进入同步代码块。同步保证了只有一个线程再同步中执行共享数据,保证安全,但牺牲了效率。
7.1 同步方法
定义同步方法解决线程安全问题
- 把访问了共享数据的代码取出来,放到一个方法中
- 在方法上添加synchronized
同步方法也会锁住方法内部,只让一个线程执行,锁对象是实现类对象new RunnableImpl(),也就是this。
public class RunnableImpl implements Runnable {//共享票源private int tickets = 100;@Overridepublic void run() {while(true){sell();}}public synchronized void sell(){if(tickets > 0){try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}String name = Thread.currentThread().getName();System.out.println("窗口"+name+"在卖第"+tickets+"张票");tickets--;}}
}
7.2 静态同步方法
加static关键字,锁对象不是this,this是创建对象之后产生的,static优先于对象的创建,静态同步方法的锁对象是本类的class属性—>class文件对象(反射)
RunnableImpl.class
7.3 Lock锁
JDK1.5之后出现Lock接口,实现了synchronized方法和语句,可获得更广泛的锁操作。
- 在成员位置创建一个ReentrantLock对象
- 在可能会出现安全问题的代码前调用Lock接口的lock方法获取锁
- 在可能会出现安全问题的代码后调用Lock接口的unlock方法释放锁
public class RunnableImpl implements Runnable {//共享票源private static int tickets = 100;Lock lock = new ReentrantLock();@Overridepublic void run() {while(true){lock.lock();if(tickets > 0){try {Thread.sleep(10);String name = Thread.currentThread().getName();System.out.println("窗口"+name+"在卖第"+tickets+"张票");tickets--;} catch (InterruptedException e) {e.printStackTrace();}finally {lock.unlock();}}}}
}
8 线程状态
- 新建状态:new
- 运行状态:runnable
- 阻塞状态:blocked
- 死亡状态:terminated
- 休眠状态:time_waiting【等待时间】
- 永久等待:waiting【等待唤醒】
new —> start() + CPU —> runnable
new —> start() - CPU —> blocked
runnable —> stop() / run(over) —> terminated
runnable —> sleep / wait —> timed_waiting
timed_waiting —> time over - CPU—> blocked
timed_waiting —> time over + CPU—> runnable
runnable —> Object.wait() —> waiting
waiting —> Object.notify() + CPU —> runnable
waiting —> Object.notify() - CPU —> blocked
9 线程通信
9.1 等待唤醒案例
- 创建消费者线程:申请资源种类和数量,调用wait方法,放弃CPU,进入waiting状态。
- 创建生产者线程:产生资源之后,调用notify唤醒消费者。
- 两个线程必须被同步代码块包裹,确保只有一个在执行。
- 同步的锁对象必须唯一,只有锁对象可以调用wait和notify方法。
public class WaitAndNotify {public static void main(String[] args) {//锁对象Object obj = new Object();//消费者new Thread(){@Overridepublic void run() {while(true){synchronized (obj){System.out.println("申请资源");try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("拿到资源");System.out.println("------------");}}}.start();//生产者new Thread(){@Overridepublic void run() {while (true){try {System.out.println("准备资源");Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}synchronized (obj){obj.notify();System.out.println("资源已备好");}}}}.start();}
}
Object.wait(long m):无参数的wait需要等待notify唤醒,有参数的wait等到时间结束后,进入到runnable(有CPU)或者blocked(无CPU)状态,相当于sleep(long m),但如果时间结束前,notify被调用,则提前醒来。
Object.notifyAll():唤醒监视器上所有的线程。
9.2 生产者和消费者案例
- 包子
public class BaoZi {String pi;//包子皮String xian;//包子馅boolean flag = false;//是否有包子
}
- 包子铺
public class BaoZiPu extends Thread {private BaoZi bz;//锁对象public BaoZiPu(BaoZi bz){this.bz = bz;}@Overridepublic void run() {int count = 0;//持续生产包子while(true){synchronized (bz){if(bz.flag == true){try {bz.wait();} catch (InterruptedException e) {e.printStackTrace();}}//被唤醒后执行 包子铺生产包子if(count % 2 == 0){bz.pi = "薄皮";bz.xian = "三鲜";}else {bz.pi = "厚皮";bz.xian = "牛肉";}count++;System.out.println("包子铺正在生产:"+bz.pi+bz.xian+"包");try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}bz.flag = true;bz.notify();System.out.println(bz.pi+bz.xian+"包已生产好,吃货可以开始吃包子了");}}}
}
- 吃货
public class ChiHuo extends Thread {private BaoZi bz;public ChiHuo(BaoZi bz){this.bz = bz;}@Overridepublic void run() {while(true){synchronized (bz){if(bz.flag == false){try {bz.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("吃货正在吃"+bz.pi+bz.xian+"包");//吃完包子bz.flag = false;//唤醒包子铺bz.notify();System.out.println("吃货已经吃完了"+bz.pi+bz.xian+"包,包子铺开始生产包子");System.out.println("===================================================");}}}
}
- 测试类
public class Test {public static void main(String[] args) {BaoZi bz = new BaoZi();BaoZiPu bzp = new BaoZiPu(bz);ChiHuo ch = new ChiHuo(bz);bzp.start();//生产包子ch.start();//吃包子}
}
10 线程池
10.1 概念
线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
线程池的优点:
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
10.2 线程池的使用
JDK1.5出现线程池的工厂类Executor用来生产线程池
Executors类的静态方法:
- newFixedThreadPool(int nThread):创建可重用固定线程数的线程池,返回值是ExecutorService接口的实现类对象,使用ExecutorService接口接收【面向接口编程】
ExecutorService接口:
- shutdown:关闭销毁线程池
- submit(Runnable task):提交一个Runnable任务用于执行
使用步骤
- 使用工厂类Executors里面的静态方法newFixedThreadPool生产一个线程池
- 创建一个实现类,实现Runnable,重写run,设置线程任务
- 调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法。
- 调用ExecutorService中的方法shutdown销毁线程池【不建议销毁线程池】
public class RunnableImpl implements Runnable {@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"创建了一个新的线程执行");}
}
public class ThreadPool {public static void main(String[] args) {ExecutorService es = Executors.newFixedThreadPool(2);es.submit(new RunnableImpl());es.submit(new RunnableImpl());es.submit(new RunnableImpl());es.submit(new RunnableImpl());}
}
pool-1-thread-2创建了一个新的线程执行
pool-1-thread-1创建了一个新的线程执行
pool-1-thread-2创建了一个新的线程执行
pool-1-thread-1创建了一个新的线程执行