多线程
程序、进程、线程的区别
- 程序:本地文件
- 进程:正在运行的程序,进程至少有一个线程
- 线程:一个进程中的多个“同时”进行的任务,两个以上线程称为多线程
多线程的“同时”运行:多线程并非是同时运行的。CPU负责执行线程,而一个CPU在一段时间内只能运行一个线程,之所以会形成“同时”运行的假象,原因在于CPU切换线程的速率极其快(毫秒单位),假设现在有A、B、C三个线程:
- A线程运行10ms
- B线程运行10ms
- C线程运行10ms
三个线程之间来回切换,在外界看来是同时运行,这样的行为叫做并发,真正的同时运行叫做并行
- 并发:在一段时间内来回切换任务,造成同时运行的假象
- 并行:所有任务同时运行
实现多线程的方式
实现多线程有三种方式。
继承Thread类
package day20191203;
public class Demo01 {
public static void main(String[] args) {
Thread t = new MyThread01();
/**
* 注意:开启线程需要调用的是start(),而不是重写后的run()
* start():自动调用run()
*/
t.start();
}
}
class MyThread01 extends Thread{
@Override
public void run() {
/**
* run():线程执行的任务
*/
for(int i=0;i<100;i++) {
System.out.println(this.getName()+":"+i);
}
}
}
实现Runnable接口
package day20191203;
public class Demo02 {
public static void main(String[] args) {
/**
* 声明线程对象时需要传入一个实现了Runnable接口的类对象作为参数
*/
Thread t = new Thread(new MyThread02());
t.start();
}
}
class MyThread02 implements Runnable{
@Override
public void run() {
for(int i=0;i<100;i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
匿名内部类实现线程
package day20191203;
public class Demo03 {
public static void main(String[] args) {
/**
* 优势:使用比较自由
* 劣势:只能使用一次
*/
Thread t1 = new Thread() {
public void run() {
for(int i=0;i<100;i++) {
System.out.println(this.getName()+":"+i);
}
}
};
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<100;i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
});
t2.start();
}
}
主方法也是一个线程,主方法结束不意味着程序结束,所有前置线程都结束才标志着程序结束
多线程的特点
-
执行顺序随机:写在前面的线程不一定先执行,写在后面的线程不一定后执行,CPU执行线程的顺序完全随机。
-
执行时间随机:CPU分配给线程的时间片长短是完全随机的,没有任何规律可以寻找。这种情况导致的结果就是程序每一次运行的输出结果都不完全相同。
-
执行过程不连续:由于CPU分配的时间片是随机的,所以无法保证线程能在一个时间片完成任务,一个任务极有可能被分割成多次完成。
时间片:由CPU分配给线程的最大运行时间,时间片结束则暂停当前线程的运行,将运行权交给另外的线程,被暂停的线程回到就绪状态等待下一次被分配。
线程的生命周期
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JGSrpIDD-1576547042891)(D:\BaiDu\BaiduNetdiskDownload\MyNotes\pic\Thread.jpg)]
线程的API
- Thread.currentThread():静态方法,用于获得当前线程对象。
package day20191208;
public class Demo02 {
public static void main(String[] args) {
// System.out.println(Thread.currentThread().getName());
// doIt();
Thread t = new Thread() {
public void run() {
System.out.println(Thread.currentThread().getName());
doIt();
}
};
t.start();
}
public static void doIt() {
System.out.println(Thread.currentThread().getName());
}
}
使用该方法,我们可以得到结论:执行该方法的线程,就是调用该方法的线程,不会发生变化。
- Thread.yield():当前线程让出cpu的时间片交给其它线程执行,类似于模拟了一次cpu切换。
- Thread.sleep(long millons):令当前线程进入休眠状态[millons]毫秒,时间一到,线程自动苏醒。
package day20191208;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Demo03 {
/**
* 简单时钟的制作
* @param args
*/
public static void main(String[] args) {
Thread t = new Thread() {
public void run() {
while(true) {
Date date = new Date();
SimpleDateFormat sf = new SimpleDateFormat("HH:mm:ss");
String str = sf.format(date);
System.out.println(str);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
};
t.start();
}
}
- getName():获得当前线程对象的名称。
- getID():获得当前线程对象的ID。
- getPriority():获得当前线程的优先级。
- setPriority():设置当前线程的优先级(1~10),1(MIN_PRIORITY)最小,10(MAX_PRIORITY)最大,每个线程的优先级默认值是5。优先级越高,被分配到时间片的可能性越大;反之,被分配到时间片的可能性越小。
- isDaemon():判断线程是否是一个守护线程。
- isInterrupted():判断线程的休眠是否被打断
- isAlive():判断线程是否是一个活跃线程
- join():阻塞当前线程,等待方法调用者结束后再执行当前线程
package day20191208;
public class Demo04 {
public static void main(String[] args) {
Thread t1 = new Thread() {
public void run() {
for(int i=0;i<100;i++) {
System.out.println("正在下载:"+i+"%");
}
System.out.println("下载完成");
}
};
Thread t2 = new Thread() {
public void run() {
try {
/**
* 阻塞当前线程t2,当方法调用者t1结束后才会执行t2
*/
t1.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("显示图片!");
}
};
t1.start();
t2.start();
}
}
守护线程
守护线程又叫后置线程,与前置线程同时执行,当所有前置线程结束时守护线程随之结束(必定结束)
package day20191208;
public class Demo01 {
public static void main(String[] args) {
Thread t1 = new Thread() {
public void run() {
for(int i=0;i<10;i++) {
System.out.println(this.getName()+":"+i);
}
}
};
Thread t2 = new Thread() {
public void run() {
for(int i=0;true;i++) {
System.out.println(this.getName()+":"+i);
}
}
};
t1.start();
/**
* 开启守护线程
*/
t2.setDaemon(true);
t2.start();
}
}
JDK中已存在的守护线程:GC
线程的同步
两个线程共享数据,互相之间竞争资源(可能引发线程不安全的问题)
package day20191208;
public class Demo05 {
public static void main(String[] args) {
Table t = new Table();
Thread t1 = new Thread() {
public void run() {
while(true) {
t.getBean();
}
}
};
Thread t2 = new Thread() {
public void run() {
while(true) {
t.getBean();
}
}
};
t1.start();
t2.start();
}
}
class Table{
int bean = 20;
public void getBean() {
if(bean == 0) {
Thread.yield();
throw new RuntimeException("豆子没了!");
}
System.out.println(Thread.currentThread().getName()+":"+bean--);
}
}
此案例将可能出现的问题:
-
两个线程获取相同的bean
-
无法结束程序
第一个问题的原因在于线程获取的时间片长短是不确定的,t1线程在完成输出即将进行bean–操作时,时间片结束,cpu切换到t2执行任务,如此一来有可能导致bean的输出混乱,所以这样的代码不算真正的线程同步。
第二个问题的原因与第一个问题相同,有可能过短的时间片使程序直接越过0这个阈值,导致无法正常结束程序(虽然改成“<=0”就能解决,但是那样就看不到线程锁的效果了)。
实现线程同步的方法
加上线程锁,实现排队执行任务(同一时间只允许一个线程工作),这样一来线程不安全变成了线程安全。
关键字:synchronized
- 方法上加锁
class Table{
int bean = 20;
public synchronized void getBean() {
if(bean == 0) {
Thread.yield();
throw new RuntimeException("豆子没了!");
}
System.out.println(Thread.currentThread().getName()+":"+bean--);
}
}
如果将此看成是一个试衣间,那么加锁就相当于拴上试衣间的门锁,同一时间只准有一个线程进入此方法,其它线程只能等待进入方法的线程结束后才能进入(这也是线程安全效率低,线程不安全效率高的原因)。
- 锁住代码块
class Table{
int bean = 20;
public void getBean() {
/**
* 锁住代码块时需要传入被锁的对象(被锁的是方法就传入所属的类对象)
*/
synchronized(this){
if(bean == 0) {
Thread.yield();
throw new RuntimeException("豆子没了!");
}
System.out.println(Thread.currentThread().getName()+":"+bean--);
}
}
}
加锁的原则:锁的范围越小越好
死锁
资源没有被正常释放,使后续线程无法进入方法,导致程序不能正常结束。
package day20191208;
public class Demo06 {
public static void main(String[] args) {
Method m = new Method();
Thread t1 = new Thread() {
public void run() {
m.a();
}
};
Thread t2 = new Thread() {
public void run() {
m.b();
}
};
t1.start();
t2.start();
}
}
class Method{
Object o = new Object();
Object k = new Object();
public void a() {
synchronized(o) {
System.out.println(Thread.currentThread().getName()+":a");
b();
}
}
public void b() {
synchronized(k) {
System.out.println(Thread.currentThread().getName()+":b");
a();
}
}
}
从上面得案例中,我们可以提取以下信息:
-
a()锁了o对象,执行完a才能释放o,但是要执行完a()必须先执行b()
-
b()锁了k对象,执行完b才能释放k,但是要执行完b()必须先执行a()
-
t1线程调用了a(),t2线程调用了b()
运行程序,我们可以发现:o对象被t1线程所占,等待k对象被t2线程释放;k对象被t1线程所占,等待o对象被t1线程释放。两个线程互不相让,都在等待对方释放资源,由此发生了死锁。
线程同步的核心问题
如何解决资源抢占的问题
线程的wait()与notify()
- wait():线程进入阻塞状态
- wait(long time):线程进入阻塞状态[time]毫秒
- notify():唤醒进入阻塞状态的线程,与wait()配合使用
- notifyAll():唤醒所有进入阻塞状态的线程
注意:使用wait()、wait(long time)、notify()、notifyAll()时,需要加锁
package day20191208;
public class Demo07 {
public static void main(String[] args) {
/**
* 1.三种功能:加载图片、显示图片、下载图片
* 2.显示图片必须在加载完成之后才能执行
* 3.显示图片可以和下载图片同时执行
*/
Object o = new Object();
Thread t1 = new Thread() {
public void run() {
System.out.println("正在加载");
for(int i=0;i<100;i++) {
System.out.println("加载进度:"+i+"%");
}
synchronized(o) {
o.notify();
}
System.out.println("正在下载");
for(int i=0;i<100;i++) {
System.out.println("下载进度:"+i+"%");
}
System.out.println("下载完成!");
}
};
Thread t2 = new Thread() {
public void run() {
synchronized(o) {
try {
o.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("显示图片!");
}
};
t1.start();
t2.start();
}
}
wait()进入的阻塞状态不同于join()和sleep()。wait()带锁进入阻塞状态后会将锁释放,被唤醒后会被重新上锁。
简单比喻:柜台前排队办事时发现材料没带够,于是在一旁等待家人将材料送过来,等待的时间内后面排队的人轮流办事,材料送到后插队继续之前的工作。
线程池
作用
- 控制线程数量:规定了内部线程的数量
- 重用线程:线程执行完任务后不会进入死亡状态,而是获取正在队列中的任务后,重新进入执行状态
创建线程池
关键词:Executors
- Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
- Executors.newFixedThreadPool(int nThreads):创建一个可重用固定线程集合的线程池,以共享的无界队列方式来运行这些线程
- Executors.newScheduledThreadPool(int corePoolSize):创建一个线程池,它会在给定延迟后运行命令或者定期执行
- Executors.newSingleThreadExecutors() :创建一个使用单个worker线程的Executors,以无界队列方式来运行该线程(单例模式)
无界队列:没有规范的,谁先抢到就给谁使用
package day20191208;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo08 {
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(2);
//创建一个线程池,内部线程数量为2
for(int i=0;i<10;i++) {
Runnable run = new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName()+":"+"i");
}
};
//只能传入Runnable对象,Thread对象本身就是一个线程
es.execute(run);
}
es.shutdown();
}
}
package day20191208;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo08 {
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(2);
//创建一个线程池,内部线程数量为2
for(int i=0;i<10;i++) {
Runnable run = new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName()+":"+"i");
}
};
//只能传入Runnable对象,Thread对象本身就是一个线程
es.execute(run);
}
es.shutdown();
}
}