目录
1 进程(process)
在操作系统中独立运行的程序,每运行一个应用程序就对应着一个进程。简单说就是你每打开一个软件就是一个进程。
打开电脑的任务管理器就能看到当前你的电脑都在运行哪些进程
多进程:在操作系统中可以同时运行多个程序,比如你可以一边听音乐一边打代码一边下载电影。
2 CPU时间片
CPU时间片就是CPU分配给各个程序的时间,即该进程允许运行的时间,但时间很短。
- 对于单核CPU:
从表面上看各个程序是同时运行的,实际上CPU在同一时间只能运行一个程序,只是因为CPU在很短时间内在不同程序间切换,轮流执行每一个程序,执行速度很快,所以看上去像是同时运行。 - 对于多核CPU,在同一时间确实是同时运行多个程序。
3 线程(thread)
3.1 简介
线程就是进程内部的一个执行单元,没有进程就没有线程。用来执行应用程序中的某一个功能。比如说打开微信这就是一个进程,打开之后你可以进行聊天、刷朋友圈等等功能,这些功能都是由线程来控制的。
多线程:在一个应用程序中可以同时执行多个功能,比如说你可以用迅雷同时下载多个东西,还有更好用的下载神器IDM,在下载一个东西的时候可以将其分成多个部分,每个部分就是一个线程,每个线程同时独立下载最终再拼接成完整的内容。
进程与线程的关系:
- 一个进程中可以包含多个线程,且至少要有一个线程(主线程)。在Java程序中 ,JVM启动时会自动创建一个主线程,用来执行main方法,还会再创建一个垃圾回收线程gc
- 一个线程必须属于某一个进程,进程是线程的容器
- 一个进程中的多个线程共享该进程的所有资源(内存)
3.2 创建线程
3.2.1 继承Thread类
步骤:
- 定义一个类,继承自Thread类
- 重写run()方法。我们创建一个新的线程是要干一些事情的,而这些事情通过重写hread类中的run()方法即可
- 创建该类的实例对象,也就是创建了一个新的线程
- 调用start()方法(该方法继承自Thread类)启动线程,之后便会自动调用run()方法。如果直接调用run()方法的话那就和调用普通方法没有区别了并没有启动线程
public class 继承Thread类 {
public static void main(String[] args) {
MyThread mt = new MyThread("多线程") ; //创建线程
mt.start(); //启动线程,不一定启动后就立刻执行,要等待获取CPU时间片
Thread t = Thread.currentThread() ; //得到当前线程对象,此处是main中,则是主线程对象
t.setName("主线程"); //设置线程名字
for(int i=0; i<100; i++)
System.out.println(t.getName()+i); //主线程默认名字为main
}
}
class MyThread extends Thread {
public MyThread() {
super();
}
public MyThread(String name) { //给线程创建名字
super(name);
}
public void run() { //重写run方法,实现需要实现的功能
for(int i=0; i<100; i++)
System.out.println(getName()+i); //getName()可以获取线程名字,默认为Thread-编号
}
}
在这个代码中,刚开始运行时先创建了一个主线程,然后执行,在执行过程中又创建了一个新的线程并启动。之后就是这两个线程“同时”执行循环输出的功能
可以看到主线程循环输出了一部分之后,变成了新线程循环输出,之后又交替重复这个过程,而且每次运行的结果可能都不一样。这就是之前说到过的CPU时间片,在某个时刻轮到主线程了便输出一部分,时间到了又轮到新线程输出。
3.2.2 实现Runnable接口
步骤:
- 定义一个类,实现Runnable接口
- 重写run()方法,将要实现的功能写入其
- 创建该类的实例对象
- 创建一个Thread类的实例对象,将上一步中创建的对象传入
- 调用线程对象的start方法,启动新线程
public class 实现Runnable方法 {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable() ;
Thread t = new Thread(mr) ; //将实现Runnable接口的对象传入
t.start();
Thread temp = Thread.currentThread() ; //得到当前线程对象,此处是main中,则是主线程对象
temp.setName("主线程"); //设置线程名字
for(int i=0; i<100; i++)
System.out.println(temp.getName()+i); //主线程默认名字为main
}
}
class MyRunnable implements Runnable {
public void run() {
for(int i=0; i<100; i++)
System.out.println(Thread.currentThread().getName()+i);
//getName方法是线程里才有,要显示线程名字就要先得到当前线程对象
}
}
3.3.3 两种方式对比
继承Thread类:
- 线程执行的代码放在Thread子类的run方法中
- 无法再继承其他类
实现Runnable接口:
- 线程执行的代码放在实现Runnable接口的类的run方法中
- 可以继承其他类,避免了单继承的局限性
- 适合多个相同程序代码的线程去处理同一个资源,比如多个窗口同时卖100张票,在哪个窗口买票都是一样的但是票的总数是固定的。
- 增强程序的健壮性
3.3 线程的生命周期
- 创建线程对象
- 调用start()方法进入就绪状态
- 当获取CPU时间片时,进入运行状态
- 若时间片用完而代码没有执行完时,重新回到就绪状态等待下次获取CPU时间片
- 若代码需要等待用户输入、调用sleep()方法让线程进入休眠、有其他线程加入等情况时,进入阻塞状态
- 当用户输入完毕、到达休眠时间、其他线程已经执行完、被其他线程中断时,回到就绪状态
- 若线程需要锁但没有获取到锁时,进入到锁池
- 当线程获取到锁时,进入到就绪状态
- 调用wait()方法,让线程放弃锁,进入等待池
- 当其他线程调用notify()或notifyAll()或等待超时,进入锁池
- 当代码执行完后,销毁线程
方法 | 功能 |
---|---|
start() | 启动线程,进入就绪状态 |
sleep(long millis) | 休眠线程,进入阻塞状态。单位为毫秒 |
yield() | 暂停执行线程,进入就绪状态 |
join() | 暂停执行线程,等待另外一个线程执行完毕之后再执行,进入阻塞状态。 |
interrupt() | 中断线程的休眠或等待状态 |
3.4 线程的优先级
线程优先级的范围在[1, 10],.
- Thread.MAX_PRIORITY:最大优先级10
- Thread.MIN_PRIORITY:最小优先级1
- Thread.NORM_PRIORITY: 默认优先级5
方法 | 功能 |
---|---|
setPriority(int newPriority) | 设置线程优先级 |
getPriority() | 获取线程优先级 |
优先级高的不一定优先处理,而是执行的概率会增大
3.5 线程安全问题
多个线程同时访问共享数据时可能会出现问题:
当多线程共享数据时,由于CPU切换,导致一个线程只执行了关键代码的一部分,还未执行完。 此时另一个线程参与进来,导致共享数据发生异常
解决方法:使用线程同步机制synchronized+锁。使关键代码不中断、连续执行
- 被synchronized包围的代码块被称为同步代码块,被其修饰的方法称为同步方法。只需要将关键代码放到同步代码块中,不要将run()方法中所有代码都添加进去,否则就相当于是单线程了。
- 锁(对象锁),每一个对象都自带一个锁(标识)且每个对象的锁都不一样
当线程执行同步代码块或同步方法时,必须获取特定对象的锁才能执行。一旦对象的锁被获取,则该对象就不再拥有锁,直到线程执行完同步代码块后才能归还锁。如果线程无法获取特定对象上的锁,则线程会进入该对象的锁池中等待,直到锁被归还后需要该锁的线程会进行竞争。
简单说就是进入第一个线程后要执行同步代码块中的代码就把锁拿走了,如果该线程休眠了切换到其他线程时,如果也想执行同步代码块里的内容就必须要拿着锁才能执行,但是由于第一个线程执行同步代码块时已经把锁拿走了,此时已经没有锁了,没有锁就打不开同步代码块的门,就执行不了其中的内容。当第一个线程执行完后就将锁归还,之后其他线程就可以拿到锁再次执行同步代码块了。
public class Test {
public static void main(String[] args) {
Ticket ticket = new Ticket() ;
Thread t1 = new Thread(ticket, "窗口1") ;
Thread t2 = new Thread(ticket, "窗口2") ;
Thread t3 = new Thread(ticket, "窗口3") ;
t1.start();
t2.start();
t3.start();
}
}
//同步代码块
public class Ticket implements Runnable{
private int num = 100 ;
Object obj = new Object() ; //自定义一个锁,每一个线程必须用同一个锁
public void run() {
System.out.println(Thread.currentThread().getName()+"正在卖票");
while(true) {
//同步代码块,保护共享数据的安全
synchronized(obj) { //获取obj上的锁才能执行
if(num > 0) {
try {
Thread.sleep(10);
}
catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"剩余" + num);
--num ;
} //执行完自动归还锁
}
}
}
}
//同步方法
public class Ticket implements Runnable{
private int num = 100 ;
Object obj = new Object() ; //自定义一个锁,每一个线程必须用同一个锁
public void run() {
System.out.println(Thread.currentThread().getName()+"正在卖票");
while(true) {
sellTicket() ;
}
}
public synchronized void sellTicket() { //同步方法,使用this锁
if(num > 0) {
try {
Thread.sleep(10);
}
catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"剩余" + num);
--num ;
}
}
}
- 线程同步的优点:解决了线程安全的问题,使代码块在某一时间只能被一个线程访问
- 线程同步的缺点:由于需要进行锁的判断,消耗资源、效率降低
3.6 线程间的通信
每个对象都自带锁池和等待池,且每个对象的都不一样
锁池:
- 当线程执行synchronized块时如果无法获取特定对象上的锁,则会进入该对象的锁池
- 当锁被归还给该对象时,锁池中的多个线程会竞争获取该对象的锁
- 获取对象锁的线程将执行synchronized块,执行完毕后会释放锁
等待池:
- 当线程获取对象的锁后,可以调用wait()方法放弃锁,此时进入对象的等待池
- 当其他线程调用该对象的notify()或notifyAll()方法时,等待池中的线程会被唤醒进入该对象的锁池
- 当线程获取对象的锁后,将从它上次调用wait()方法的位置开始继续运行
方法 | 功能 |
---|---|
wait() | 使线程放弃对象锁,线程进入等待池。可在参数上设置等待时间,超时便会自动唤醒 |
notify() | 随机唤醒等待池中的一个线程(唤醒的是特定对象的等待池中的线程) |
notifyAll() | 唤醒等待池中的所有线程 |
这三个方法都只能在synchronized块中使用(只有获取了锁的线程才能调用),等待和唤醒必须使用同一个对象
public class Test2 {
public static void main(String[] args) {
Object obj = new Object() ; //所有线程使用同一个对象
Wait w = new Wait(obj) ; //进入等待池
w.start();
try {
w.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Notify n = new Notify(obj) ; //唤醒等待池中线程
n.start();
}
}
public class Wait extends Thread{
private Object obj ;
public Wait(Object obj) {
this.obj = obj ;
}
public void run() {
System.out.println("123");
synchronized(obj) {
try {
System.out.println(getName()+"释放锁,即将进入等待池");
obj.wait(); //进入obj对象的等待池,放弃锁
} catch (InterruptedException e) {
System.out.println(getName()+"被中断");
}
}
System.out.println("456");
}
}
public class Notify extends Thread{
private Object obj ;
public Notify(Object obj) {
this.obj = obj ;
}
public void run() {
synchronized(obj) {
obj.notifyAll(); //唤醒所有线程
System.out.println(getName()+"已经唤醒等待池中的线程");
}
}
}
3.7 生产者-消费者问题
是多线程同步的一个经典问题,即并发协作的问题。包含了两种线程:生产者线程、消费者线程
生产者线程:
- 生产商品并放入缓冲区
- 当缓冲区满时,生产者不可再生产商品
消费者线程:
- 从缓冲区中取出商品(生产者和消费者使用的是同一个缓冲区)
- 当缓冲区为空时,消费者不可再取出商品
public class Test {
public static void main(String[] args) {
ProductPool pool = new ProductPool() ; //生产者消费者都用同一个缓冲区
Producer p1 = new Producer("p1", pool) ;
Producer p2 = new Producer("p2", pool) ;
Producer p3 = new Producer("p3", pool) ;
Consumer c1 = new Consumer("c1", pool) ;
Consumer c2 = new Consumer("c2", pool) ;
p1.start() ;
p2.start() ;
p3.start() ;
c1.start() ;
c2.start() ;
}
}
public class Producer extends Thread{ //生产者
private String name ;
private ProductPool pool ; //缓冲区
public Producer(String name, ProductPool pool) {
this.name = name ;
this.pool = pool ;
}
public void run() {
while(true) {
synchronized(pool) { //同步代码块
if(pool.isFull()) { //生产满了就暂停生产
try {
pool.wait();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
else { //否则就一直生产
pool.put();
System.out.println(name+"生产了一个商品,现在商品数量为:"+pool.getNum());
pool.notifyAll(); //当生产出商品后就可以通知消费者购买
}
}
try {
Thread.sleep(3000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Consumer extends Thread { //消费者
private String name ;
private ProductPool pool ; //缓冲区
public Consumer(String name, ProductPool pool) {
this.name = name ;
this.pool = pool ;
}
public void run() {
while(true) {
synchronized(pool) {
if(pool.isEmpty()) { //如果没货了就不能买了
try {
pool.wait();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
else { //有货就买
pool.get();;
System.out.println(name+"购买了一个商品,现在商品数量为:"+pool.getNum());
pool.notifyAll(); //商品减少就通知生产者生产
}
}
try {
Thread.sleep(2000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ProductPool { //缓冲区
private int num ; //商品数量
private static final int MAX_COUNT = 20 ; //最大数量
public void put() { //生产商品
++num ;
}
public void get() { //买商品
--num ;
}
public int getNum() {
return num ;
}
public boolean isEmpty() { //缓冲区是否空了
return num==0 ;
}
public boolean isFull() { //缓冲区是否满了
return num==MAX_COUNT ;
}
}