1. 概念
进程 | 正在进行中的程序,运行一个程序就启动了一个或多个进程 |
---|---|
线程 | 一个程序内部的执行路径,如QQ管家的杀毒和清理垃圾功能 |
单线程 | 一个进程中只有一个执行路径,如Java中的Main方法 |
多线程 | 一个进程中有多个线程在极短的时间内交替进行 |
多进程 | 在操作系统中能同时运行多个程序 |
tips:每个进程都有独立代码和数据空间(进程上下文),进程间切换开销大。
tips:同一进程内的多个线程共享相同的代码和数据空间,线程间切换开销小。
2. 线程
线程分为前台线程和后台线程。
前台线程中,先执行主线程,然后主线程随机分配时间片,最后子线程交替运行。
后台线程也叫守护线程,后台线程是依赖前台线程的,如果所有的前台线程都死了,那么后台线程自动退出(JVM就退出了),只要有一个前台线程活动,那么后台线程就活动。
2.1 前台线程
继承Thread类方式创建线程
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 99; i++) {
System.out.println(i);
}
}
}
public class TestMyThread {
public static void main(String[] args) {
Thread t1 = new MyThread();
Thread t2 = new MyThread();
t1.start();
t2.start();
}
}
tips:继承的方式比较单一,因为无法再去继承其他的类。
Thread类常用方法
public void start();// 手动启动线程(并不一定立刻执行,等JVM随机分配时间片);
public final Boolean isAlive();// 测试线程是否还活着;
public static Thread currentThread();// 返回当前正在执行的线程对象的引用;
public final void setName(String name);// 设置该线程的名字
public final String getName();// 返回该线程的名称;
public final void setPriority(int newPriority);// 设置线程优先级;[MAX_PRIORITY][NORM_PRIORITY][MIN_PRIORITY]
public final int getPriority();// 获取线程优先级
public static void sleep(long millis);// 在指定的毫秒数内让当前正在执行的线程休眠;
实现Runnable接口方式创建线程
class MyRun implements Runnable {
@Override
public void run() {
for (int i = 0; i < 99; i++) {
System.out.println(i);
}
}
}
public class ThreadTest {
public static void main(String[] args) {
MyRun myRun = new MyRun();
Thread t1 = new Thread(myRun);
t1.start();
}
}
Thread匿名内部类的方式创建线程
new Thread() {
public void run() {
for (int i = 1; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}.start();
Runnable匿名内部类的方式创建线程 - 方式1
Runnable r = new Runnable() {
@Override
public void run() {
for (int i = 1; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
};
new Thread(r).start();
Runnable匿名内部类的方式创建线程 - 方式2
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}).start();
2.2 后台线程
守护线程测试案例:主线程循环50次,子线程无限循环 将子线程设置成守护线程,如果主线程结束之后,子线程也结束,则子线程设置守护成功
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + "..run..");
}
}
});
t1.setDaemon(true);// 把t1设置成了守护线程,如果想把某一个线程设置为守护线程,必须在启动之前去设置
t1.start();// 启动t1
for (int i = 0; i <= 50; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
3. 线程同步
就比如你和赵四一起做同一套数学模拟题,同步就是你做几道,将卷子扔给赵四,赵四做几道再扔给你,循环交替,直到卷子做完。异步就是你们将卷子复制一套,然后一人做半套题,最后拼成一份答案。
异步互不干扰,资源利用率高,因为整个过程中没有人会长时间处于等待状态,但是不安全,因为有可能两个人题目刷重。
同步安全,不会刷重题目,但是效率相对而言会低一些,但有些时候,我们不得不牺牲一点效率因素,来提升安全因素。
为了共享区域的安全,我们在写程序时可以通过关键字synchronized来加保护伞。synchronized主要应用于同步代码块和同步方法中,以保证该方法在运行的时候不会被打断。又因为线程同步的实现主要是利用到了对象锁,所以我们还可以利用对象锁来实现同步;
售票系统案例
public class TicketSellTest{
public static void main(String[] args) {
MyThread1 mt = new MyThread1();
Thread t1 = new Thread(mt);
Thread t2 = new Thread(mt);
t1.setName("窗口1");
t2.setName("窗口2");
t1.start();
t2.start();
}
}
class MyThread1 implements Runnable {
private Integer ticketNo = 1;// 票编号
@Override
public void run() {
// 为了测试明显,可以在这里睡眠1秒
while (true) {// 循环卖票
sell();
}
}
public void sell() {// 售票方法
if (ticketNo <= 100) {
System.out.println(Thread.currentThread().getName() + "卖出 : " + (ticketNo++) + "号票");
}
}
}
tips:发现因为线程之间互相争抢资源,所以会出现两个售票点重复卖票的情况。
3.1 synchronized同步方法
我们可以利用synchronized关键字来将方法同步,此时当线程A访问这个方法的时候,其他线程将无法访问这个线程,只有当线程A访问结束之后,其他线程才能接着访问。
public synchronized void sell(){...}
3.2 synchronized同步代码块
我们还可以使用synchronized(){}块来灵活的控制我们要同步的部分。
public void sell(){
synchronized(this){
if (ticketNo <= 100) {
System.out.println(Thread.currentThread().getName() + "卖出 : " + (ticketNo++) + "号票");
}
}
}
3.3 Lock同步
我们还可以用Lock来进行同步:在线程类中实例化锁对象,在sell方法中使用锁对象。
private Lock lock = new ReentrantLock();
...
public void sell(){
lock.lock();// 上锁
if (ticketNo <= 100) {
System.out.println(Thread.currentThread().getName() + "卖出 : " + (ticketNo++) + "号票");
}
lock.unlock();// 解锁
}
3.4 锁类型
同步方法的原理,其实就是锁,只有多个线程使用的是同一种锁才可能发生同步现象。
synchronized(){}的小括号中可以直接看到锁的类型,而同步方法却看不到锁的类型。
3.4.1 同步实例方法的锁
测试同步方法的锁的类型,我们让线程A走同步代码块,让线程B走同步方法,如果仍旧发生同步现象,则代表同步方法中的锁和测试中同步代码块中的锁一致。
public class 测锁 {
public static void main(String[] args) {
Runnable r = new Ticket3();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
}
}
class Ticket3 implements Runnable {
private int tickets = 1;
private boolean flag = false;
Object obj = new Object();
@Override
public void run() {
while (!flag) {
if (Thread.currentThread().getName().equals("t1")) {
// 如果t1进来了 ,我让他走[同步块]
synchronized (new Ticket3()) { // this锁
if (tickets < 100)
System.out.println(Thread.currentThread().getName() + ":" + (tickets++));
else
flag = true;
}
} else {
// 如果t2进来了,我让他走同步方法
sell();
}
}
}
public synchronized void sell() {
if (tickets < 100)
System.out.println(Thread.currentThread().getName() + ":" + (tickets++));
else {
flag = true;
}
}
}
tips:测试结果表明,同步方法的锁,就是this锁。
3.4.2 同步静态方法的锁
静态方法的锁测试原理和之前的一样。
public class 测锁 {
public static void main(String[] args) {
Runnable r = new Ticket3();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
}
}
class Ticket3 implements Runnable {
private static int tickets = 1;
private static boolean flag = false;
Object obj = new Object();
@Override
public void run() {
while (!flag) {
if (Thread.currentThread().getName().equals("t1")) {
// 如果t1进来了 ,我让他走[同步块]
synchronized (Ticket3.class) { // 静态类对象锁
if (tickets < 100)
System.out.println(Thread.currentThread().getName() + ":" + (tickets++));
else
flag = true;
}
} else {
// 如果t2进来了,我让他走同步方法
sell();
}
}
}
public static synchronized void sell() {
if (tickets < 100)
System.out.println(Thread.currentThread().getName() + ":" + (tickets++));
else {
flag = true;
}
}
}
tips:测试结果表明,同步静态方法的锁,是所属的类对象的字节码对象锁。
3.4.3 死锁
案例:吃饭的时候,我有一根筷子,你有一根筷子,我需要你给我凑成一双,我吃饭,你需要我给你凑成一双,你吃饭,这时候就会僵持不下,发生死锁。
线程也是一样,A线程持有一个B的锁,B线程持有一个A的锁,二者谁也不肯释放锁,就会发生死锁。
死锁的现象我们应该积极避免。
死锁案例
public class 死锁 {
public static void main(String[] args) {
MyThread7 mt = new MyThread7();
new Thread(mt).start();
new Thread(mt).start();
}
}
class MyThread7 implements Runnable {
private Object obj1 = new Object();// obj1锁
private Object obj2 = new Object();// obj2锁
@Override
public void run() {
if (Thread.currentThread().getName().equals("Thread-0")) {
// 线程1进来了
synchronized (obj1) {
System.out.println("if--obj1");
synchronized (obj2) {
System.out.println("if--obj2");
}
}
} else {
// 线程2进来了
synchronized (obj2) {
System.out.println("else--obj2");
synchronized (obj1) {
System.out.println("else--obj1");
}
}
}
}
}
4. 线程等待
案例:我有个资源,一个线程往这个资源里写入,另一个线程从这个资源中读出。
读写案例 - 未同步版本
public class 读写案例_未同步 {
public static void main(String[] args) {
Res res = new Res();// 共享资源,写在这里是为了让两个线程共享同一个res
WriteRes in = new WriteRes(res);// 在创建Input线程类的时候将共享资源传入
ReadRes out = new ReadRes(res);// 在创建Output线程类的时候将共享资源传入
new Thread(in).start();// 启动一个写线程
new Thread(out).start();// 启动一个读线程
}
}
class Res {
private String name;
private String gender;
public String getName() {return name;}
public void setName(String name) {this.name = name;}
public String getGender() {return gender;}
public void setGender(String gender) {this.gender = gender;}
}
class WriteRes implements Runnable {
private Res res;// 共享资源
private boolean flag = false;// 中英文切换写入标志,false时写中文,true时写英文
public Input(Res res) {// 通过构造方法获取共享资源res,并传递给当前类属性res
this.res = res;
}
@Override
public void run() {
while (true) {// 无限写
if (flag) {
res.setName("zhaosi");
res.setGender("male");
flag = false;
} else {
res.setName("赵四");
res.setGender("男");
flag = true;
}
}
}
}
class ReadRes implements Runnable {
private Res res;// 共享资源
public Output(Res res) {// 通过构造方法获取共享资源res,并传递给当前类属性res
this.res = res;
}
@Override
public void run() {
while (true) {// 无限读
System.out.print(res.getName());
System.out.print(" ---- ");
System.out.println(res.getGender());
}
}
}
案例升级:上面的代码在运行过程中会出现数据错位,比如 “赵四 — male” 或者 “zhaosi ---- 男” 的情况,如何使用同步解决这个问题。提示:让两个线程同步的前提是,两个线程可以获得相同的锁,res对象本身就是共享资源,直接拿res当锁,即可完成两个线程的同步。
读写案例 - 同步版本
在写的线程中,while循环下加synchronized (res) {}锁
while(true){
synchronized (res) {
// ...写的代码
}
}
在读的线程中,while循环下加synchronized (res) {}锁
while(true){
synchronized (res) {
// ...读的代码
}
}
案例再升级:将之前的读写案例,改为写一个,读一个,交替运行。
[方法提示]
wait() | 让某个线程等待,此时该线程会加入到线程池进行等待 |
---|---|
notify() | 让某个线程被唤醒,只能唤醒一个,而且是在线程池中随机唤醒 |
notifyAll() | 唤醒线程池中的所有等待线程 |
tips:这三个方法是Object类中的,不是Thread类中的,而且只能在同步的前提下中使用。
思路:
- 在资源类设置一个标志变量hasRes,标识当前是否有数据,默认为false
- 写线程中,写之前判断当前是否有数据
如果有数据,写线程等待res.wait()
如果没数据,执行写数据代码,并把hasRes改成true,并且唤醒读线程 - 读线程中,读之前判断当前是否有数据
如果有数据,执行写数据代码,并把hasRes改成false,并且唤醒写线程
如果没数据,读线程等待res.wait()
代码:资源类添加hasRes属性和set/get方法
class Res{
//... 其余代码略
private boolean hasRes = false;
public boolean isHasRes() { return hasRes; }
public void setHasRes(boolean hasRes) { this.hasRes = hasRes; }
}
代码:在写线程的同步代码块中添加如下代码:
while(true){
synchronized (res) {
if(res.isHasRes()){// 当前有数据
res.wait();// input等待
}else{// 当前没数据
// ...写数据的代码,略
res.setHasRes(true);// 改变标志位
res.notify();// 唤醒output
}
}
}
代码:在读线程的同步代码块中添加如下代码:
while(true){
synchronized (res) {
if(res.isHasRes()){// 有数据
// ...读数据的代码,略
res.setHasRes(false);// 改变标志位
res.notify();// 唤醒input
}else{ // 没数据
res.wait();// output等待
}
}
}
tips:sleep和wait的区别
sleep()可以理解为,你上厕所上到一半,在厕所里睡着了,这时候别人想上厕所也进不来,必须等你睡醒出来或者强行叫醒你[interreput()]。
wait()可以理解为,你刚进厕所突然发现忘了带纸,只能先出去拿纸,等一会儿再来上厕所,这个时候别人可以插队进来。区别1:这两个方法来自不同的类分别是Thread和Object。sleep是Thread的静态方法,需要当前线程来控制,而wait是作用在某个对象。
区别2:sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
区别3:wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用。
5. 停止线程
想停止一个线程,只能等待run方法结束(stop()方法已经过时),开启线程就一定跟循环相关,所以我们用更改变标志位的方法就可以结束循环,就可以结束run,而如果要被停止的线程处于挂起状态,则需要强行使用interrupt()方法来终止。
**下面我们在主线程中停止子线程。**
public class KillThread {
public static void main(String[] args) {
DeadThread deadThread = new DeadThread();// 子线程类
Thread t1 = new Thread(deadThread);// 子线程
t1.start();// 启动子线程
for (int i = 0; i < 10; i++) {// 主线程循环10次
System.out.println("主线程运行:" + i);
}
System.out.println("主线程改变子线程循环标志位");
deadThread.setDead(true);// 改变子线程循环标志位true
}
}
class DeadThread implements Runnable {
private boolean dead = false;// 线程体循环标志位
@Override
public void run() {
while (!dead) {
System.out.println("子线程运行..");
}
System.out.println("子线程结束..");
}
public boolean isDead() {return dead;}
public void setDead(boolean dead) {this.dead = dead;}
}
问题:当线程体wait或者sleep时,处于挂起状态,这时候的线程仍然存活,但是标志位的方法已经不能用了。
解决:使用interrupt方法清除挂起状态(会抛异常,可以在异常处理中更改标识符结束循环)。
步骤1:将上面案例中的deadThread.setDead(true);变成t1.interrupt();
//deadThread.setDead(true);// 改变子线程循环标志位true
t1.interrupt();// 清除子线程挂起状态,并打断
步骤2:线程体中添加synchronized并改写如下代码:
@Override
public synchronized void run() {// wait必须在同步方法中才能使用
while (!dead) {
try {
wait();// 线程挂起
} catch (InterruptedException e) {
dead = true;// 爆发异常后,改变标志位,否则子程序还是不结束
}
System.out.println("子线程运行..");
}
System.out.println("子线程结束..");
}
6. 线程插队让步
当A线程读到了B线程的join()方法,会停下来,等B死掉后,再执行,join一般用于临时加入一个线程方法,yeild()用于让出一个时间片(不明显),就比如食堂排队打饭,我调用join方法可以插队买饭,而我调用yield方法是让给你一次机会,让你排在我的前面买饭。
join()方法写在哪个线程中,就插队哪个线程。
join()方法如果写在start()之前,不报错,但是是没有插队效果的。
yield方法是静态的,使用的时候请使用Thread.yield();
public class 线程插队{
public static void main(String[] args) throws InterruptedException {
JoinThread joinThread = new JoinThread();
Thread t1 = new Thread(joinThread);
Thread t2 = new Thread(joinThread);
t1.start();// t1插队前需要先启动,否则无法完成插队效果
t1.join();// t1插队,此时主线程会挂起,直到t1死去才继续运行
t2.start();// 主线程被t1插队,所以t1执行完之前,t2不会被启动
for (int i = 0; i < 100; i++) {// 主线程
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
class JoinThread implements Runnable{
@Override
public void run() {
for(int i=0;i<10;i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
7. 线程调度类
java.util.Timer 和 java.util.TimerTask这两个类可以负责java中的定时和调度相关内容。
Timer中的schedule(TimerTask task,long delay,long period)方法负责定时,读音[si gai zhu ler],参数1是你的任务类,参数2是几毫秒后执行,参数3是周期,每隔多少毫秒执行一次。
Timer中的cancel()方法负责结束定时任务。
案例:设计我的闹钟程序,每天早上6点,闹钟响起,控制台输入"q"结束闹钟。
public class 我的闹钟{
public static void main(String[] args) throws IOException {
Timer timer = new Timer();// 定时对象
MyTask myTask = new MyTask();// 定时任务
timer.schedule(myTask, 0, 1000);// 0毫秒后,每隔1000毫秒执行一次myTask
if (System.in.read() == 'q') {
// timer.cancel(); // 关闭schedule
myTask.setFlag(false);// 闹钟设置为不响
}
}
}
class MyTask extends TimerTask {
private boolean flag = false;// 闹钟标志位 - 不响
@Override
public void run() {
String dateStr = new SimpleDateFormat("hhmmss").format(new Date());// 获取系统时间格式化成时分秒
// System.out.println(dateStr);
if ("060000".equals(dateStr)) {// 每日早晨6点
flag = true;// 闹钟标志位 - 响
}
if (flag) {
System.out.println("起床了.....");
}
}
public boolean isFlag() {return flag;}
public void setFlag(boolean flag) {this.flag = flag;}
}
- 线程的生命周期
tips:线程总是处于6种生命周期之一。