1. 线程基本概念
1.1进程
- 进程是指运行中的程序,比如启动了QQ,就相当于启动了一个进程,操作系统会为该进程分配空间;当我们使用迅雷,就相当于又启动了一个进程,操作系统将为迅雷分配新的内存空间;
- 进程是程序的一次执行过程,或是正在运行的一个程序,是动态过程,有它自身的产生、存在、消亡的过程;
- Run代表启动了一个进程,会马上进入main方法,这时进程就开启了一个线程,即主线程;
这时再启用start()方法就会在主线程里面再启动一个子线程:Thread-0;
子线程在执行的时候,主线程不会阻塞,会继续执行;
1.2线程
- 线程由进程创建的,是进程的一个实体;
- 一个进程可以拥有多个线程;
- 单线程:在同一个时刻,只允许执行一个线程;
- 多线程:同一个时刻,可以执行多个线程。比如,一个qq进程,可以同时打开多个聊天窗口;一个迅雷进程,可以同时下载多个文件;
1.3 并发
(1)单核CPU实现多任务;(2)同一个CPU,在某个时刻,多个任务交替执行;CPU速度很快,造成“QQ”和“迅雷”貌似同时执行的错觉;
1.4 并行
(1)在同一个时刻多个任务同时执行,多核CPU可以实现并行;
获取当前电脑Cpu数量
3. 线程基本使用
3.1 继承Thread类
- 当一个类继承了Thread类,该类就可以当作线程使用;
- Thread类实现了Runnable接口的run方法
- 重写run方法,实现自己的业务逻辑
- 调用start()方法会调用run()方法
3.1.1 子线程
- Run代表启动了一个进程,会马上进入main方法,这时进程就开启了一个线程,即主线程;
- 这时再启用start()方法就会在主线程里面再启动一个子线程:Thread-0;
- 子线程在执行的时候,主线程不会阻塞;
- 子线程、主线程会交替执行;
- 主线程的名称是main,子线程的名称从Thread-0开始;
3.1.1.1 JConsole监控
- 验证:使用JConsole监控线程执行情况:
3.1.1.2 直接run()方法
- 此时的run()方法只是个普通方法,直接调用并不会开启子线程,此时Thread.currentThread().getName()是main
- 直接调用run方法不会开启子线程,默认在主线程内执行;
- 会把run()方法执行完毕,才向下执行,串行化地执行,程序会阻塞;
3.1.1.2 调用start()方法
- 真正实现多线程的效果,是start0()方法
- cat调用start()方法,进入到Thread类的start()方法,start()方法调用start0()方法;
- 进入到Thread类中的start()方法;start0()是本地方法,JVM调用,用C/C++编写;
3.2 实现Runable接口
- 假如一个A类继承了B类,那么它就不能再继承Thread类,这个时候让A类实现Runable接口,同样能实现线程;
- Runable接口里没有start()方法,只有个run方法;
解决方案:dog对象赋给了Thread类中的target,thread.start()方法调用start0()方法,start0()方法调用本类的run方法,再调用target的run方法;
3.2.1 线程代理模式
用代码模拟实现Runnable接口实现线程的机制;
- 把Proxy类当作Thread类
public class Thread02 {
public static void main(String[] args) {
Tiger tiger = new Tiger();//实现了Runnable接口
ThreadProxy threadProxy = new ThreadProxy(tiger);
threadProxy.start();
}
}
实现步骤: 先创建一个Proxy类,继承Runnable
class ThreadProxy implements Runnable {//把Proxy类当作 ThreadProxy 线程代理
private Runnable target = null;
@Override
public void run() {
if (target != null) {
target.run();//动态绑定(运行类型Tiger)
}
}
public ThreadProxy(Runnable target) {
this.target = target;
}
public void start() {
start0();//真正实现多线程的方法
}
public void start0() {
run();
}
}
- 再创建一个普通类,实现Runnable接口;
class Tiger extends Animal implements Runnable{
@Override
public void run() {
System.out.println("老虎发威~~");
}
}
3.2.2 多线程机制
两个子线程会交替执行
在这里,主线程看不到
3.3 继承Thread Vs 实现Runnable接口
- 本质没有区别,因为Thread类本身就实现了Runnable接口的run()方法
- 但是实现Runnable接口的方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制;(建议使用Runnable接口)
- 继承Thread实现线程无法做到资源共享,并且受到单继承的限制;
3.3.1 售票系统超卖问题
sleep()要在–tickets前面才能超卖
public class SellTicket {
public static void main(String[] args) {
/*SellTicket1 sellTicket1 = new SellTicket1();
SellTicket1 sellTicket2 = new SellTicket1();
SellTicket1 sellTicket3 = new SellTicket1();
sellTicket1.start();
sellTicket2.start();
sellTicket3.start();*/
SellTicket1 sellTicket1 = new SellTicket1();
Thread thread1 = new Thread(sellTicket1);
Thread thread2 = new Thread(sellTicket1);
Thread thread3 = new Thread(sellTicket1);
thread1.start();
thread2.start();
thread3.start();
}
}
class SellTicket1 extends Thread {
//private static int tickets = 100;
private int tickets = 100;
int count = 0;
@Override
public void run() {
while (true) {
if (tickets <= 0) {
break;
}
System.out.println("窗台:" + Thread.currentThread().getName() +
"出售了" + (++count) + "张票,剩余票数:" + (--tickets));
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
3.3.2 线程终止
- 当线程完成任务后,会自动退出;
- 可以通过变量来控制run方法退出线程,即通知方式
3.3.2.1 通知线程退出
启动一个线程t,要求在main线程中去停止线程t
解决方案:在主线程中通过Setter方法将loop变量设置为false
3.3.3 线程基本方法
方法名 | 作用 |
---|---|
setName | 设置线程名称 |
getName | 获取线程名称 |
setPriority | 设置线程优先级 |
getPriority | 获取线程优先级 |
interrupt | 中断线程 |
3.3.3 线程中断
- interrupt中断线程,但并没有真正的结束线程,所以一般用于中断正在休眠的线程;(中断休眠,唤醒线程)
- sleep(),线程的静态方法,使当前线程休眠;
public class ThreadMethod_ {
public static void main(String[] args) throws InterruptedException {
T t = new T();
t.setName("赵志伟");//设置线程名称
t.setPriority(Thread.MIN_PRIORITY);//设置线程优先级
t.start();//启动了子线程
System.out.println(t.getName());//获取线程名称
//主线程打印5个hi,然后就中断子线程的休眠
for (int i = 0; i < 5; i++) {
Thread.sleep(1000);
System.out.println("hi~~" + i);
}
System.out.println(t.getName() + "线程的优先级" + t.getPriority());
t.interrupt();//执行到interrupt()方法时,中断线程的休眠
}
}
class T extends Thread {
@Override
public void run() {
while (true) {
for (int i = 0; i < 100; i++) {
//获取当前线程的名称
System.out.println(Thread.currentThread().getName() + " 吃包子~~" + i);
}
try {
System.out.println(Thread.currentThread().getName() + " 正在休眠中~~");
Thread.sleep(20 * 1000);
} catch (InterruptedException e) {
//当该线程执行到一个interrupt 方法时,就会catch 一个异常,可以加入自己的业务代码
// InterruptedException捕获了一个中断异常
System.out.println(Thread.currentThread().getName() + "被中断了");
}
}
}
}
3.3.4 线程礼让
yield:线程的礼让,让出cpu,让其它线程执行,但礼让的时间不确定,所以不一定礼让成功;
3.3.5 线程插队
join:线程插队,一旦插入成功,则先执行完插入的线程的所有的任务;
public class Thread03 {
public static void main(String[] args) throws InterruptedException {
T t = new T();
Thread thread = new Thread(t);
thread.start();
for (int i = 1; i <= 20; i++) {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "(小弟)吃了" + i + "个");
if (i == 10) {
System.out.println("main线程(小弟)让子线程(大哥)先吃");
// thread.join();//子线程(大哥)插队,大哥吃完小弟再吃
Thread.yield();//main线程(小弟)礼让,不一定成功...
}
}
}
}
class T implements Runnable {
private int count = 0;
private boolean loop = true;
@Override
public void run() {
while (loop) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子线程(老大)吃了" + (++count) + "个");
if (count == 20) {//吃到20个结束
System.out.println("子线程(大哥)吃完了,main线程(小弟)继续吃");
break;
}
}
}
public void setLoop(boolean loop) {
this.loop = loop;
}
}
3.3.5.1 线程插队练习
public class ThreadMethodExercise {
public static void main(String[] args) throws InterruptedException {
T1 t1 = new T1();
Thread thread = new Thread(t1);
for (int i = 1; i <= 10; i++) {
Thread.sleep(1000);
System.out.println("main线程:hi " + i);
if (i == 5) {
thread.start();
thread.join();//子线程启动后直接插队
}
}
}
}
class T1 implements Runnable {
private int count;
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子线程: hello " + (++count));
if (count == 10) {
break;
}
}
}
}
3.3.6 守护线程
用户线程:也叫工作线程、当线程的任务执行完或通知方式结束;
守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束;
常见的守护线程:垃圾回收机制;
主线程结束,子线程并不会结束;如果我们希望主线程结束时,子线程也结束,可以将子线程设置为守护线程;
public class ThreadMehod03 {
public static void main(String[] args) {
MyDaemonThread myDaemonThread = new MyDaemonThread();
Thread thread = new Thread(myDaemonThread);
thread.setDaemon(true);//将子线程设置为守护线程
thread.start();
for (int i = 1; i <= 10; i++) {
System.out.println("主线程" + Thread.currentThread().getName() + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyDaemonThread implements Runnable {
@Override
public void run() {
for (; ; ) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子线程" + Thread.currentThread().getName());
}
}
}
3.3.7 线程的声明周期
- JDK中用Thread.State枚举表示了线程的七种(六种)状态
public class ThreadState_ {
public static void main(String[] args) throws InterruptedException {
T2 t2 = new T2();
Thread thread = new Thread(t2);
System.out.println(thread.getName() + "状态 " + thread.getState());//NEW
thread.start();
//子线程和主线程同时执行
/*for (int i = 0; i < 10; i++) {
Thread.sleep(1000);//主线程休眠、主线程输出
System.out.println(thread.getName() + "状态 " + thread.getState());//RUNNABLE
}*/
while (Thread.State.TERMINATED != thread.getState()) {
Thread.sleep(1000);
System.out.println(thread.getName() + "状态 " + thread.getState());//RUNNABLE、TIMED_WAITING
}
System.out.println(thread.getName() + "状态 " + thread.getState());//TERMINATED
}
}
class T2 implements Runnable {
@Override
public void run() {
while (true) {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);//子线程输出
try {
Thread.sleep(1000);//子线程休眠
} catch (InterruptedException e) {
e.printStackTrace();
}
}
break;
}
}
}
3.3.8 线程同步机制
- 在多线程编程中,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任意一个时刻,最多有一个线程访问,以保证数据的完整性;
- 也可以这样理解:线程同步,即当有一个线程在对内存进行操作时,其它线程都不可以对这个内存地址进行操作,知道该线程完成操作后,其它线程才能对该内存地址进行操作;
3.3.9 互斥锁
-
Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性;
-
每个对象都对应一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象;
-
关键字synchronized来与对象的互斥锁联系,当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问;
-
同步的局限性:会导致程序的执行效率降低;
-
同步方法(非静态的)的锁可以是this,也可以是其它对象(要求是同一个对象)
-
同步方法(静态的)的锁为当前类本身;
-
手段:方法上加锁、代码块上加锁;
-
执行了线程对象的wait()方法,当前线程暂停,释放锁;
-
同步方法如果没有使用static修饰,默认锁对象为this;
-
如果方法使用static修饰,默认锁对象:当前类.class; 如果想在静态方法中,实现一个同步代码块,写类本身;
3.4.0 死锁
public class DeadLock_ {
public static void main(String[] args) {
DeadLockDemo deadLockDemo = new DeadLockDemo(true);
deadLockDemo.setName("A线程");
deadLockDemo.start();
DeadLockDemo deadLockDemo1 = new DeadLockDemo(false);
deadLockDemo1.setName("B线程");
deadLockDemo1.start();
}
}
class DeadLockDemo extends Thread {
static Object o1 = new Object();//保证多线程,共享一个对象,使用static
static Object o2 = new Object();
boolean flag;
public DeadLockDemo(boolean flag) {//构造器
this.flag = flag;
}
@Override
public void run() {
//(1)如果flag为true,线程A 就会先得到/持有 o1对象锁,然后尝试去获取o2对象锁
//(2)如果flag为true,线程A 就会先得到/持有 o1对象锁,然后尝试去获取o2对象锁
if (flag) {
synchronized (o1) {
System.out.println(Thread.currentThread().getName() + "进入1");
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + "进入2");
}
}
} else {
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + "进入3");
synchronized (o1) {
System.out.println(Thread.currentThread().getName() + "进入4");
}
}
}
}
}
3.4.1 释放锁
- 同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁;
- 同步方法中遇到break,return,不得不结束线程,释放锁;
- 同步方法中出现了未处理的error或者Exception,导致线程异常结束,释放锁;
3.4.1.1 不释放锁
- 程序调用Thread.sleep()、Thread.yield() 方法暂停当前线程的执行,但并不会释放锁;
- 线程执行同步代码块时,其它线程调用了该线程的suspend()方法将该线程挂起,而该线程不释放锁;