目录
一、线程的基本使用
(一)创建线程的两种方式
(二)线程简单案例(Thread)
问题:main函数与开启的线程是否是阻塞的,即线程运行时,main函数等待线程运行结束?
问题:为什么不直接调用cat的run方法,而是通过start来使用?
start解析
(三)线程简单案例(Runnable)
(四)线程简单案例(多线程)
二、线程终止
三、线程的常用方法
(一)常用方法一
(二)常用方法二
(三)用户线程和守护线程
四、线程生命周期
五、线程的同步
六、互斥锁
七、线程死锁
八、释放锁
(一)下面操作会释放锁
(二)下面操作不会释放锁
一、线程的基本使用
(一)创建线程的两种方式
在Java中线程使用有两种方式:
① 继承Thread类,重写run方法
② 实现Runnable接口,重写run方法
(二)线程简单案例(Thread)
由主函数开启一个线程,每秒钟输出一句话
public class Thread01 {
public static void main(String[] args) {
Cat cat = new Cat(); // 创建对象当成线程使用
cat.start(); // 调用start方法开启线程
}
}
// 1.继承Thread类后就可以当作线程使用
// 2.重写run方法,实现自己的业务
// 3.Thread类实现了Runnable的run方法
class Cat extends Thread {
static int count = 0;
@Override
public void run() { // 重写run方法,实现业务
while(true) {
count ++;
// 每隔一秒,输出一句话
System.out.println("我是帅哥" + count);
// 让该线程休眠1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(count == 8) break;
}
}
}
问题:main函数与开启的线程是否是阻塞的,即线程运行时,main函数等待线程运行结束?
验证
(1)方式一
首先改进main函数,运行
public static void main(String[] args) throws InterruptedException {
Cat cat = new Cat(); // 创建对象当成线程使用
cat.start(); // 调用start方法开启线程
for (int i = 0; i < 10; i++) {
System.out.println("主线程运行" + i);
Thread.sleep(1000);
}
}
查看两个线程的名字
System.out.println("主线程运行" + i + "线程名字" + Thread.currentThread().getName());
System.out.println("我是帅哥" + count + "线程名字" + Thread.currentThread().getName());
说明主线程与开启的线程是并行的,即多线程
(2)方式二
使用Jconsole(jdk自带的线程监控工具)查看线程,首先扩大输出语句的次数方便我们验证
public class Thread01 {
public static void main(String[] args) throws InterruptedException {
Cat cat = new Cat(); // 创建对象当成线程使用
cat.start(); // 调用start方法开启线程
for (int i = 0; i < 60; i++) {
System.out.println("主线程运行" + i);
Thread.sleep(1000);
}
}
}
// 1.继承Thread类后就可以当作线程使用
// 2.重写run方法,实现自己的业务
// 3.Thread类实现了Runnable的run方法
class Cat extends Thread {
static int count = 0;
@Override
public void run() { // 重写run方法,实现业务
while(true) {
count ++;
// 每隔一秒,输出一句话
System.out.println("我是帅哥" + count);
// 让该线程休眠1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(count == 80) break;
}
}
}
运行main函数后,在Terminal输入Jconsole
main线程和Thread-0线程都在运行
main线程运行结束后,Thread-0线程还在继续运行
Thread-0线程运行结束,此时连接中断
总结:当main函数运行结束时,不意味着main函数开启的线程也结束。
问题:为什么不直接调用cat的run方法,而是通过start来使用?
解释:如果调用cat,run()来执行,此时run方法里输出的线程名字将会是main,意味着并没有开启线程,而且此时是串行化的执行,需要run方法运行结束才会继续执行main下面的内容
验证
public static void main(String[] args) throws InterruptedException {
Cat cat = new Cat(); // 创建对象当成线程使用
//cat.start(); // 调用start方法开启线程
cat.run(); // 直接调用run方法
for (int i = 0; i < 60; i++) {
System.out.println("主线程运行" + i + "线程名字" + Thread.currentThread().getName());
Thread.sleep(1000);
}
}
start解析
查看start方法中,我们可以发现调用了start0方法,该方法是一个native(本地)方法,由JVM调用,底层是c/c++实现,真正实现多线程的效果, 是 start0(), 而不是 run
public synchronized void start() {
。。。
start0();
。。。
}
private native void start0();
(三)线程简单案例(Runnable)
java是单继承的,在某些情况下一个类可能已经继承了父类,此时无法通过继承Thread类创建线程,因此可以通过实现Runnable接口来创建线程 ,这里使用了代理模式,通过Thread类进行代理,最终调用的是实现Runnable接口的dog的run方法。
public class Thread02 {
public static void main(String[] args) {
Dog dog = new Dog();
// 此时dog没有start方法
// 创建Thread对象,将实现Runnable方法的dog放入,调用start方法
Thread thread = new Thread(dog);
thread.start();
}
}
class Dog implements Runnable {
static int count = 0;
@Override
public void run() {
while(true) {
System.out.println("狗叫" + (++ count) + " 线程名称" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
(四)线程简单案例(多线程)
public class Thread03 {
public static void main(String[] args) {
T1 t1 = new T1();
T2 t2 = new T2();
Thread thread1 = new Thread(t1);
thread1.start(); // 启动线程1
Thread thread2 = new Thread(t2);
thread2.start(); // 启动线程2
}
}
class T1 implements Runnable {
static int count = 0;
@Override
public void run() {
while(true) {
System.out.println("t1..." + (++ count) + " 线程名称" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(count == 10) {
break;
}
}
}
}
class T2 implements Runnable {
static int count = 0;
@Override
public void run() {
while(true) {
System.out.println("t2..." + (++ count) + " 线程名称" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(count == 10) {
break;
}
}
}
}
总结:继承Thread VS 接口Runnable
从java的设计来看,通过继承Thread或者实现Runnable接口来创建线程本质上没有区别,都是start()-->start0(), 从jdk帮助文档我们可以看到Thread类本身就实现了Runnable接口
实现Runnable接口方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制,建议使用Runnable
使用继承方式的好处是,在 run() 方法内获取当前线程直接使用 this 就可以了,无须使用Thread.currentThread0方法;不好的地方是 Java 不支持多继承,如果继承了Thread类那么就不能再继承其他类。另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码,而 Runable 则没有这个限制。
二、线程终止
当线程完成任务后会自动退出,也可以通过使用变量控制run方法退出的方式停止线程,即通知方式
public class Thread01 {
public static void main(String[] args) throws InterruptedException {
ThreadExit threadExit = new ThreadExit();
threadExit.start();
// 休眠5秒再通知threadExit线程
Thread.sleep(5 * 1000);
// 将flag置为false,停止threadExit线程
threadExit.setFlag(false);
}
}
class ThreadExit extends Thread {
static int count = 0;
private Boolean flag = true;
@Override
public void run() {
while(flag) {
System.out.println("线程运行" + (++count) + " 线程名字" + this.getName());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void setFlag(Boolean flag) {
this.flag = flag;
}
}
三、线程的常用方法
(一)常用方法一
- setName //设置线程名称,使之与参数 name 相同
- getName //返回该线程的名称
- start //使该线程开始执行; Java 虚拟机底层调用该线程的 start0 方法
- run //调用线程对象 run 方法,直接调用不会创建新的线程
- setPriority //更改线程的优先级
- getPriority //获取线程的优先级
- sleep //线程的静态方法,在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
- interrupt //中断线程,但没有真正终止线程,一般用于中断正在休眠的线程
线程优先级
案例:
public class ThreadMethod01 {
public static void main(String[] args) throws InterruptedException {
T t = new T();
t.setPriority(1);
t.start();
// 休眠5秒叫醒线程t
Thread.sleep(5000);
System.out.println("线程" + t.getName() + "的优先级为:" + t.getPriority());
t.interrupt();
}
}
class T extends Thread {
@Override
public void run() {
while(true) {
System.out.println("线程" + this.getName() + "吃一百个包子");
try {
System.out.println("睡个觉继续吃...");
sleep(20 * 1000);
} catch (InterruptedException e) {
System.out.println("被叫醒了,继续吃");
}
}
}
}
(二)常用方法二
- yield:线程的礼让。让出cpu,让其他线程执行,但礼让的时间不确定,所以也不一定礼让成功
- join: 线程的插队。插队的线程一旦插队成功,则肯定先执行完插入的线程所有的任务
join解释图
案例: main线程创建一个子线程,每隔1s 输出 hello输出 20次,主线程每隔1秒,输出 hi,输出 20次.要求两个线程同时执行,当主线程输出 5次后,就让子线程运行完毕,主线程再继续
public class ThreadMethod02 {
public static void main(String[] args) throws InterruptedException {
T1 t1 = new T1();
t1.start();
for (int i = 1; i <= 10; i++) {
System.out.println("main线程吃 " + "吃第" + i + "个包子");
Thread.sleep(1000);
if(i == 5) {
System.out.println("老大" + t1.getName() + "先吃");
t1.join(); // 让t1先运行完
System.out.println("老大" + t1.getName() + "吃完了");
}
}
}
}
class T1 extends Thread {
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(this.getName() + "吃第" + i + "个包子");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
yield
public static void main(String[] args) throws InterruptedException {
T1 t1 = new T1();
t1.start();
for (int i = 1; i <= 10; i++) {
System.out.println("main线程吃 " + "吃第" + i + "个包子");
Thread.sleep(1000);
if(i == 5) {
System.out.println("老大" + t1.getName() + "先吃");
// t1.join();
Thread.yield(); // 线程礼让
System.out.println("老大" + t1.getName() + "吃完了");
}
}
}
(三)用户线程和守护线程
- 用户线程:也叫工作线程,当线程的任务执行完或通知方式结束
- 守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束
- 常见的守护线程:垃圾回收机制
案例:子线程循环次数大于main线程,当main线程结束后,子线程也需要结束
public class ProtectThread {
public static void main(String[] args) throws InterruptedException {
T4 t4 = new T4();
t4.setDaemon(true);
t4.start();
for(int i = 0; i < 5; i ++) {
System.out.println("我是用户线程(工作线程)");
Thread.sleep(1000);
}
}
}
class T4 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("我是守护线程...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
四、线程生命周期
有的书籍会说7种状态,是将Runnable状态分为了Ready(就绪态)和Running(运行态)
查看线程状态
public class ThreadState_ {
public static void main(String[] args) throws InterruptedException {
T t = new T();
System.out.println(t.getName() + " 状态 " + t.getState());
t.start();
while (Thread.State.TERMINATED != t.getState()) {
System.out.println(t.getName() + " 状态 " + t.getState());
Thread.sleep(500);
}
System.out.println(t.getName() + " 状态 " + t.getState());
}
}
class T extends Thread {
@Override
public void run() {
while (true) {
for (int i = 0; i < 2; i++) {
System.out.println("hi " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
break;
}
}
}
五、线程的同步
在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多有一个线程访问,以保证数据的完整性。
也可以这里理解:线程同步,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作.
可以 通过加锁实现线程的同步,比如Synchronized关键字
1、同步代码块
synchronized(对象) { // 得到对象的锁, 才能操作同步代码
// 需要同步的代码
}
2、同步方法
Synchronized还可以放在方法声明中,表示整个方法为同步方法
public synchronized void m() {
// 同步代码段
}
六、互斥锁
- Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。
- 每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
- 关键字synchronized 来与对象的互斥锁联系。当某个对象用synchronized修饰时, 表明该对象在任一时刻只能由一个线程访问
- 同步的局限性:导致程序的执行效率要降低
- 同步方法(非静态的)的锁可以是this,也可以是其他对象(要求是同一个对象)
- 同步方法(静态的)的锁为当前类本身
注意事项和细节:
- 同步方法如果没有使用static修饰:默认锁对象为this
- 如果方法使用static修饰,默认锁对象:当前类.class
- 实现的落地步骤:
- 需要先分析上锁的代码
- 选择同步代码块或同步方法
- 要求多个线程的锁对象为同一个即可!
七、线程死锁
多个线程都占用了对方的锁资源,但不肯相让,导致了死锁,在编程是一定要避
免死锁的发生.
案例:
妈妈:你先完成作业,才让你玩手机
小明:你先让我玩手机,我才完成作业
public class DeadLock {
public static void main(String[] args) {
//模拟死锁现象
DeadLockDemo A = new DeadLockDemo(true);
A.setName("A 线程");
DeadLockDemo B = new DeadLockDemo(false);
B.setName("B 线程");
A.start();
B.start();
}
}
class DeadLockDemo extends Thread {
static Object o1 = new Object();// 保证多线程,共享一个对象,这里使用 static
static Object o2 = new Object();
private boolean flag;
public DeadLockDemo(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if(flag) {
synchronized (o1) {
System.out.println(this.getName() + "进入了1" + " 请求2");
synchronized (o2) {
System.out.println(this.getName() +" 请求2");
}
}
} else {
synchronized (o2) {
System.out.println(this.getName() + "进入了3" + " 请求4");
synchronized (o1) {
System.out.println(this.getName() +" 请求4");
}
}
}
}
}