多线程
多条路走、提高效率。线程是独立执行的路径
基本概念
进程:主要指运行在内存中的可执行文件,多进程可以让操作系统同时执行多个任务。进程是重量级的,会消耗cpu、内存等系统资源,进程数量比较局限。
线程:线程是进程内部的程序流,每个进程内部都支持多线程,线程是轻量的,新建线程会共享所在进程的系统资源。
多线程是采用时间片轮转法来保证多个线程的并发执行,所谓并发就是指宏观并行微观串行的机制
并行:多个CPU同时执行多个任务,比如:多个人同时做不同的事
并发:一个CPU(采用时间片)同时执行多个任务,多个人做同件事
线程实现(重点)
线程创建(重中之重)
- java.lang.Thread类代表线程,任何线程对象都是Thread类(子类)的实例
- Thread类是线程的模板,封装了复杂的线程开启等操作,封装了操作系统的差异性
创建方式
- 自定义类继承Thread类并重写run方法,然后创建该类的对象调用start方法
- 自定义类实现Runnable接口并重写run方法,创建该类的对象作为实参来构造Thread类型的对象,然后使用Thread类型的对象调用start方法。
相关方法

Thread类中Run方法测试
如果不重写run方法
public class ThreadTest {
public static void main(String[] args) {
//1.使用无参方式构造Thread类型的对象
//由源码可知:Thread类中成员变量target的数值为null
Thread test1 = new Thread();
//2.调用run方法进行测试
//由源码可知:由于成员变量target中数值为null,因此条件if(target!=null)不成立,跳过{}中代码不执行
//而run方法除了上述代码再无代码,因此证明run方法确实啥也不敢
test1.run();
//3.打印一句话
System.out.println("看看你是否啥也不干!");
}
}

重写run方法
public class SubThreadRun extends Thread{
@Override
public void run() {
//打印1-20
for(int i=1;i<=200;i++){
System.out.println("Run方法中i="+i);
}
}
}
public class SubThreadRunTest {
public static void main(String[] args) {
//1.生命Thread类型的引用指向子类型的对象
Thread t1 = new SubThreadRun();
//2.调用run方法测试
//t1.run();
t1.start();//用start可用于启动线程Java虚拟机自动调用该类线程中的run方法
for(int i=1;i<=200;i++){
System.out.println("----------main方法中i="+i);
}
//一般用start不用run,run方法相当于对普通成员方法的调用,因此执行run方法代码后才能向下执行
}
}
结果如下

- 执行main方法的线程叫做主线程,执行run方法的线程叫做新线程/子线程
- main方法是程序的入口,对于start方法之前的代码来说,由主线程执行一次,当start方法调用成功后线程的个数由1个变成了2个,新启动的线程去执行run方法的代码,主线程继续向下执行,两个线程各自独立运行互不影响。
- 当run方法执行完毕后子线程结束,当main方法执行完毕后主线程结束
- 两个线程执行没有明确的先后执行次序,由操作系统调度算法来决定
线程创建方式一继承Thread类(重点)的实现
即将一个类声明为Thread的子类,重写run()方法,调用start开启线程
public class testThread1 extends Thread{
@Override
public void run() {
//run方法线程体
for (int i = 0; i < 10; i++) {
System.out.println("run方法线程输出:"+i);
}
}
public static void main(String[] args) {
//main方法线程体。主线程
//调用一个线程对象,调用start方法
testThread1 test = new testThread1();
test.start();//start方法开启线程
for (int i = 0; i < 10; i++) {
System.out.println("主线程正在输出:"+i);
}
}
}
这样run方法和主方法会一起执行

实现Runnable接口(重点)(核心)(比第一种用得多)
自定义类实现Runnable接口并重写run方法,创建该类的对象作为实参来构造Thread类型的对象,然后使用Thread类型的对象调用start方法。
另一种说法:实现Runnable接口,实现run()方法,编写线程执行体,创建线程对象调用Start()方法启动线程
public class testthread2 implements Runnable{
@Override
public void run() {
//run方法线程体
for (int i = 0; i < 100; i++) {
System.out.println("run方法线程输出:"+i);
}
}
public static void main(String[] args) {
//创建Runnable接口实现类对象
testthread2 test = new testthread2();
//创建线程对象,通过线程对象来开启我们的线程,代理
//Thread thread = new Thread();
//thread.start();
new Thread(test).start();
for (int i = 0; i < 1000; i++) {
System.out.println("主线程正在输出:"+i);
}
}
}
启动线程:传入目标对象+Thread对象.start()
实现多个线程同时操作一个对象
public class TestThread3 implements Runnable{
private int ticket = 10;
@Override
public void run() {
while(true){
if(ticket<=0){
break;
}
//模拟延时
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"拿到了第"+ticket--+"张票");
}
}
public static void main(String[] args) {
TestThread3 ticket1 = new TestThread3();
new Thread(ticket1,"小明").start();
new Thread(ticket1,"小红").start();
new Thread(ticket1,"老师").start();
}
}
结果:

发现问题:多个 线程同时操作一个资源的时候,线程不安全,数据紊乱
线程安全(重要)
public class testthread2 implements Runnable{
private int ticket = 100;
@Override
public void run() {
while(true){
if(ticket<=0){
break;
}
//模拟延时
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"拿到了第"+ticket--+"张票");
}
}
public static void main(String[] args) {
testthread2 ticket1 = new testthread2();
new Thread(ticket1,"小明").start();
new Thread(ticket1,"小红").start();
new Thread(ticket1,"老师").start();
}
}

原因:

某个线程尚未操作完成,有其他线程进来,也操作车票。其实就是共享数据了
锁:一个线程在操作票的时候,其他线程不能参与进来。这种情况就算当前线程被阻塞了也不能被改变
方法一:同步代码块 synchronized
synchronized(同步监视器){
//需要被同步的代码
}
说明:操作共享数据的代码就是需要被同步的代码
共享数据:多个线程共同操作的变量,上面的ticket就是。只有共享数据的多个线程才会出现线程安全问题
同步监视器就是锁,任何一个类的对象都可以充当锁
public class testthread2 implements Runnable{
private int ticket = 100;
Object obj = new Object();
@Override
public void run() {
while(true){
synchronized(obj) {
if (ticket <= 0) {
break;
}
//模拟延时
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"拿到了第"+ticket--+"张票");
}
}
public static void main(String[] args) {
testthread2 ticket1 = new testthread2();
new Thread(ticket1,"小明").start();
new Thread(ticket1,"小红").start();
new Thread(ticket1,"老师").start();
}
}
锁的要求:多个线程必须共用同一把锁,在上面的代码中反应的就是obj要是唯一的
上面的代码就是创建一个对象ticket1,三个线程共用一个对象,在ticket1中有只有唯一的一个obj熟悉,所以就共用同一个锁
同步的方式解决了线程安全的问题,但是在操作同步代码时,只能有一个线程参与,其他线程等待,相当于是一个当线程的过程,效率会低
- synchronized(obj)可以改为synchronized(this) this表示当前对象,因为 testthread2 ticket1 = new testthread2();只造了一个对象,所以是唯一的(testthread2对象)锁
但是当用继承自Thread类来创建多线程的时候
会创建多个对象
所以不能synchronized(this)
**解决方式:1.**用当前的类充当锁
如synchronized(类名.class) 因为类也是对象,且类这个对象只会加载一次
2. 创建一个private static obj;
方法二:同步方法解决实现Runnable接口
如果操作共享数据的代码正好都在一个方法中,那么我们可以把此方法声明为同步的
public synchronized void 方法名() {
i++;
}
在此方法中监视器就是this
public class testthread2 implements Runnable{
private int ticket = 100;
Object obj = new Object();
@Override
public void run() {
while(true){
show();
}
}
public synchronized void show(){
if (ticket > 0) {
//模拟延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"拿到了第"+ticket--+"张票");
}
}
public static void main(String[] args) {
testthread2 ticket1 = new testthread2();
new Thread(ticket1,"小明").start();
new Thread(ticket1,"小红").start();
new Thread(ticket1,"老师").start();
}
}
方法二:同步方法解决继承Thread类解决线程安全问题
这里格外注意同步监视器的问题,用static关键字解决
public class testThread1 extends Thread{
private static int ticket = 100;
@Override
public void run() {
//run方法线程体
while(true){
show();
}
}
private static synchronized void show(){
if(ticket>0){
//模拟延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"拿到了第"+ticket--+"张票");
}
}
public static void main(String[] args) {
//main方法线程体。主线程
//调用一个线程对象,调用start方法
testThread1 test1 = new testThread1();
testThread1 test2 = new testThread1();
testThread1 test3 = new testThread1();
/* test1.setName("窗口1");
test1.setName("窗口2");
test1.setName("窗口3");*/
test1.start();//start方法开启线程
test2.start();//start方法开启线程
test3.start();//start方法开启线程
}
}
同步方法存在的问题:效率低与死锁
效率低:在synchronized中只能有单个线程在里面
死锁:不同的线程分别占用对方需要的同步资源不放弃,都在等对方放弃自己需要的同步资源
使用Lock(锁)
lock本身是个接口
public class LockTest implements Runnable{
private int ticket = 100;
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true) {
try{
//调用lock
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "拿到了第" + ticket + "张票");
ticket--;
} else {
break;
}
}finally{
//调用解锁方法
lock.unlock();
}
}
}
public static void main(String[] args) {
LockTest ticket1 = new LockTest();
Thread t1 = new Thread(ticket1,"小明");
Thread t2 = new Thread(ticket1,"小红");
Thread t3 = new Thread(ticket1,"老师");
t1.start();
t2.start();
t3.start();
}
}
为什么会有线程安全问题:
因为进程是CPU中分配资源的基本单位,而线程共享进程中资源,如内存地址,当多个线程访问同一个内存地址,且内存地址是可变的时候,就容易发生线程安全问题
面试题1:
synchronized与lock方式的异同:
同:二者都可以解决线程安全问题
不同:
- 首先synchronized是java内置关键字,在jvm层面,Lock是个java类
- synchronized执行完同步代码会自动释放,Lock要手动启动同步,同时结束同步也需要手动实现
- synchronized有代码块锁和方法锁,lock只有代码块锁
- lock性能更好,多用
面试题2:如何解决线程安全问题?有几种方式
synchronized(同步代码块和同步方法)和lock
线程的通信
wait/notify机制
- wait():让当前线程 释放对象锁并进入等待(阻塞)状态(执行wait会释放锁)
- notify():一旦执行此方法就会唤醒被wait的一个线程,多个线程被wait就唤醒优先级高的那个
- notifyAll()——唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行
实现两个线程交替打印1到100
class Number implements Runnable{
private int number = 1;
@Override
public void run() {
while(true) {
synchronized (this) {
notify();
if (number < 100) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
try {
//使得调用wait()的线程进入阻塞状态
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.start();
t2.start();
}
}

梳理过程:首先当线程1拿到锁进入后,到了wait()状态,阻塞了并释放锁。这时候线程二拿到锁进来,notify唤醒了线程1,线程1进入就绪状态。然后线程2自身进入wait(),以此类推
注意
- 这三个方法都要在同步代码块或同步方法中,在lock中都不行
- 这三个方法都是由同步监视器发起调用
- 这三个方法都是定义在Object类中的,因为任何对象都要有这个方法(任何对象都可以充当同步监视器)
面试题
- sleep()方法和wait方法的异同?
相同点: 一旦执行方法都可以让当前线程进入阻塞状态
不同点:1)两个方法声明的位置不同:Thread类中声明的sleep(),Object()中声明的wait()
- 调用范围不同:sleep()可以在任何场景下调用,wait()必须使用在同步代码块或同步方法中
3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会
使用线程池的方式
提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用
龟兔赛跑
- 首先要有赛道距离,离终点越来越近
- 判断比赛是否结束
- 打印出胜利者
- 龟兔赛跑开始
- 乌龟赢,兔子需要睡觉,所以我们来模拟兔子睡觉
- 最终乌龟赢了
public class Race implements Runnable{
private static String winner;//胜利者
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
//模拟兔子休息
if(Thread.currentThread().getName().equals("兔子")&& i%10==0){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//判断比赛是否结束
boolean flag = gameOver(i);
if(flag){
break;
}
System.out.println(Thread.currentThread().getName()+"跑了"+i+"步");
}
}
//判断是否完成比赛
private boolean gameOver(int step){
if(winner!=null){
return true;
}if(step>=100){
winner = Thread.currentThread().getName();
System.out.println("Winner is :"+winner);
return true;
}
return false;
}
public static void main(String[] args) {
Race race = new Race();
new Thread(race,"乌龟").start();
new Thread(race,"兔子").start();
}
}
匿名内部类实现线程创建和启动
public class ThreadNoNameTest {
public static void main(String[] args) {
//匿名内部类语法格式:父类/接口类型 引用变量名 = new 父类/接口类型() {方法的重写};
//1.使用继承加匿名内部类的方式创建并启动线程
/*Thread t1 = new Thread() {
@Override
public void run() {
System.out.println("张三说:在吗?");
}
};*/
new Thread() {
@Override
public void run() {
System.out.println("张三说:在吗?");
}}.start();
//2.使用实现接口加匿名内部类的方式创建并启动线程
Runnable ra = new Runnable() {
@Override
public void run() {
System.out.println("李四说:不在。");
}
};
Thread t2 = new Thread(ra);
t2.start();
}
}
线程状态
生命周期

线程的编号和名称

public class ThreadIdNameTest extends Thread{
public ThreadIdNameTest(String name){
super(name);//表示调用父类的构造方法
}
@Override
public void run() {
System.out.println("子线程的编号是:"+getId()+",名称是:"+getName());
//修改名称为"张飞“
setName("zhangfei");
System.out.println("修改后的子线程的编号是:"+getId()+",名称是:"+getName());
}
public static void main(String[] args) {
ThreadIdNameTest tint = new ThreadIdNameTest("guanyu");
tint.start();
//获取当前正在执行线程的引用,当前正在执行的线程是主线程,也就是获取主线程的引用
Thread t1 = Thread.currentThread();
System.out.println("主线程的编号是:"+t1.getId()+",名称是:"+t1.getName());
}
}

终止线程的三种方式
1.使用 interrupt() 中断线程,但是 interrupt() 并不会理解结束线程,而是给在当前线程中打一个停止的标记,也就是说,线程中断并不会立即终止线程,而是通知目标线程,有人希望你终止。至于目标线程收到通知后会如何处理,则完全由目标线程自行决定。
2. 用 stop() 终止线程,不安全,不使用它
3.使用标志位终止线程
停止线程时不推荐使用stop()、destroy()等方法,建议使用一个标志位进行中止变量,当flag=false,则中止线程运行
//设置标准位停止线程
public class TestStop implements Runnable{
//设置标识位
private boolean flag = true;
@Override
public void run() {
int i = 0;
while(flag){
System.out.println("running"+i++);
}
}
//设置一个公开的方法停止线程,转换标识位
public void stop(){
this.flag = false;
}
public static void main(String[] args) {
TestStop teststop = new TestStop();
new Thread(teststop).start();
for (int i = 0; i < 1000; i++) {
System.out.println("main"+i);
if(i==900){
//切换标志位,让线程停止
teststop.stop();
System.out.println("这个线程停止,只有主线程在跑");
System.out.println(i);
}
}
}
}
常用的方法

826

被折叠的 条评论
为什么被折叠?



