一 简述
二 概念
三 实现方法
public class Test { public static void main(String[] args) { //创建线程对象 MyThread t1 = new MyThread(); MyThread t2 = new MyThread(); //启动线程 t1.start(); t2.start(); } } class MyThread extends Thread{ //run中代码,为线程在开启后执行的代码 public void run(){ for (int i = 0; i < 5; i++) { System.out.println("线程开启了" + i); } } }
运行结果:俩个线程之间的运行顺序由CPU决定,不存在固定顺序
为什么要重写run()方法?因为run()方法是用来封装被线程执行的代码
实现Runnable接口
定义一个类MyRunnable实现Runnable接口
在MyRunnable中重写run()方法
创建MyRunnable对象
创建Thread类对象,把MyRunnable对象作为构造方法的参数
启动线程
public class Test { public static void main(String[] args) { //创建线程对象,通过实现Runnable接口的对象作为参数 Thread t1 = new Thread(new MyRunnable()); Thread t2 = new Thread(new MyRunnable()); //启动线程 t1.start(); t2.start(); } } class MyRunnable implements Runnable{ //run中代码,为线程在开启后执行的代码 public void run(){ for (int i = 0; i < 5; i++) { System.out.println("线程开启了" + i); } } }
定义一个类MyCallable实现Callable<>接口,注意:Callable接口存在泛型表达式,其泛型定义的类为 call()方法中的返回值类型
在MyCallable类中重写call()方法(与run()方法类似)但是call方法存在线程结束的返回值
创建MyCallable类的对象
使用FutureTask<>类创建对象,参数为MyCallable类的对象,泛型类与Callable相同。在FutureTask中存在get()方法,用来接收线程结束的返回值。注意:在一段代码开始运行后:先使用main线程调用main方法,如果使用get那么需要对应线程结束运行后才能得到结果,否则将死等结果,造成代码无法停止
创建Thread对象,把FutureTask<>类对象作为构造方法参数
启动线程
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; public class Test { public static void main(String[] args) throws Exception { //创建MyCallable类对象 MyCallable mc = new MyCallable(); //通过MyCallable类对象创建FutureTask类对象 FutureTask<String> ft1 = new FutureTask<>(mc); FutureTask<String> ft2 = new FutureTask<>(mc); //通过FutureTask对象创建Thread类对象 Thread t1 = new Thread(ft1); Thread t2 = new Thread(ft2); //启动线程 t1.start(); t2.start(); //输出线程结束的结果 System.out.println(ft1.get()); System.out.println(ft2.get()); } } class MyCallable implements Callable<String> { //call中代码,为线程在开启后执行的代码,与run方法不同,其存在返回值 public String call() throws Exception { for (int i = 0; i < 5; i++) { System.out.println("线程开启了" + i); } return "MyCallable类"; } }
优点 | 缺点 | |
实现Runnable 或Callable接口 | 扩展性强,实现该接口的同时 还可以继承其他类 | 编程相对复杂,不能直接 使用Thread类中的方法 |
继承Thread类 | 编程比较简单,可以直接使用 Thread类中的方法 | 可扩展性较差, 不能继承其他类 |
四 Thread类常用方法
1 String getName(); 返回线程的名字 注:线程有默认的名字,格式:Thread-编号(没有设置名字的线程,启动时编号由0开始,逐步加一) 2 void setName(String name); 将此线程的名称更改为参数name 注:通过构造方法也可以设置线程名字,但如果使用继承与Thread类的子类时, 需要在子类中重写带参构造的方法,因为子类的构造方法需要写入super关键字调用父类的构造方法。 而不会默认继承 3 public static Thread currentThread(); 返回对当前正在执行的线程对象的引用 注:使用场景在继承接口时,其没有继承Thread类则不能使用getName()方法, 可以先使用currentThread获得对象,再使用getName() :Thread.currentThread().getName() 4 public static void sleep(long time); 让线程休眠指定的时间,单位为毫秒 注:其为类方法,单个对象调用只会使得该类休眠 在继承Thread类和实现接口的方法中,使用sleep()方法,必须使用try-catch解决异常,不能使用throw 5 public final void setDaemon(boolean on); 设置为守护线程 注:当普通线程结束时,守护线程也会随之结束
五 多线程的实现
1 public final void setPriority(int newPriority); 设置线程的优先级 2 public final int getPriority(); 获取线程的优先级 注:优先级 1 - 10;默认值为 5 优先级越高,只是抢夺到CPU的执行权的机率更高,不是绝对的
六 线程的安全问题
public class Test { public static void main(String[] args) throws Exception { Ticket t1 = new Ticket(); Ticket t2 = new Ticket(); Ticket t3 = new Ticket(); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } } //创建Ticket类,实现多线程 class Ticket extends Thread{ //票数 private int ticket = 100; public void run(){ while (true){ if (ticket == 0){ System.out.println("票买完了"); break; } else { ticket--; System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩下" + ticket + "张票"); } } } }
效果图:从图中可以看出,实际上三个窗口并没有关联到一起,各卖各的,相当于一共卖了300张票
public class Test { public static void main(String[] args) throws Exception { //创建实现Runnable接口的对象 Ticket ticket = new Ticket(); //使该对象作为唯一参数,创建Thread对象,保证线程共用一个参数 Thread t1 = new Thread(ticket); Thread t2 = new Thread(ticket); Thread t3 = new Thread(ticket); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } } //创建Ticket类,实现多线程 class Ticket implements Runnable{ //票数 private int ticket = 100; public void run(){ while (true){ if (ticket == 0){ System.out.println("票买完了"); break; } else { ticket--; System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩下" + ticket + "张票"); } } }
效果图:虽然打印的次序与顺序不一,但是三者相关联,总票数为100,但同时也存在着出现重复票的问题,在下面代码中通过阻塞方法sleep进行分析
public class Test { public static void main(String[] args) throws Exception { //创建实现Runnable接口的对象 Ticket ticket = new Ticket(); //使该对象作为唯一参数,创建Thread对象,保证线程共用一个参数 Thread t1 = new Thread(ticket); Thread t2 = new Thread(ticket); Thread t3 = new Thread(ticket); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } } //创建Ticket类,实现多线程 class Ticket implements Runnable{ //票数 private int ticket = 100; public void run(){ while (true){ if (ticket <= 0){ System.out.println("票买完了"); break; } else { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } ticket--; System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩下" + ticket + "张票"); } } } }
结果演示:出现很多相同票,并且出现负票
结果分析:在代码运行期间,任何时候3个线程都可能出现CPU的抢占使用,并不是一个线程对象在一次代码完整结束后,才会进行下一个代码。这样就会导致出现,同时对票数 private int ticket 的操作,导致运行结果问题的产生。延迟100毫秒,使得问题可以被放大化。
问题解决
问题分析:多线程同时操作共享数据导致
解决思路:使多线程不能同时对共享数据操作,将程序锁起来,即使获得了CPU使用权,若已有线程进行操作,则该线程仍不能使用代码
实现方式:将操作共享数据的多条代码锁起来,让任意时刻只能有一个线程执行。Java中提供了同步代码块的方式来解决
同步代码块:锁多条语句的代码块,可以使用同步代码块实现:
synchronized(任意对象/锁对象){ 多条语句操作共享的数据代码 }
该代码锁:默认情况下是打开的 ,但只要有一个线程进去执行代码了,锁就会关闭,当线程执行完,锁才会自动打开
同步/锁的好处:解决了多线程的数据安全问题
同步/锁的弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
代码演示:
public class Test { public static void main(String[] args) throws Exception { //创建实现Runnable接口的对象 Ticket ticket = new Ticket(); //使该对象作为唯一参数,创建Thread对象,保证线程共用一个参数 Thread t1 = new Thread(ticket); Thread t2 = new Thread(ticket); Thread t3 = new Thread(ticket); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } } //创建Ticket类,实现多线程 class Ticket implements Runnable { //票数 private Integer ticket = 100; //创建对象作为锁 private Object obj = new Object(); public void run() { while (true) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (obj) { //锁对象 if (ticket <= 0) { System.out.println("票买完了"); break; } else { ticket--; System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩下" + ticket + "张票"); } } } } }
效果图:可以看出,没有出现同一票俩次购买的问题,同时打印顺序也与票数相对应
注意事项:只将操作到共享数据的代码放入 synchronized同步块中,(sleep代码放在synchronized()方法外,因为sleep若在synchronized中使用,则该线程休眠时,不会自动释放锁,导致其他线程无法操作)同时synchronized()的参数为一个锁对象,要确保多个线程使用的是同一把锁(即同使用同一个对象作为参数)
若使用的是继承Thread类的方法实现多线程时,一定要确保共享数据,以及锁全是静态数据,才能保证在new一个新对象时,不会创建新的共享数据与锁
//静态票数 private static Integer ticket = 100; //静态接口 private static Object obj = new Object();
这是同时new三个对象
Ticket t1 = new Ticket(); Ticket t2 = new Ticket(); Ticket t3 = new Ticket();
使用的仍然时共享数据与锁,可以达到多线程要求
同步方法:
修饰符 synchronized 返回值类型 方法名(参数列表){方法体}
1 void lock(); 获得锁 2 void unlock(); 释放锁
Lock是接口不能被实例化,可采用它的实现类ReentrantLock来创建对象
代码演示:
synchronized(任意对象/锁对象){ 多条语句操作共享的数据代码 } 转化为: //创建ReentrantLock对象 ReentrantLock lock = new ReentrantLock(); //上锁 lock.lock(); 多条语句操作共享的数据代码 //释放锁 lock.unlock();
为了防止代码中间报错,而没有释放锁,可将unlock()放入,finally中
ReentrantLock lock = new ReentrantLock(); public void run() { while (true) { //解决sleep使用try - catch - finally try { Thread.sleep(1000); lock.lock(); 多条语句操作共享的数据代码 } catch (Exception e) { e.printStackTrace(); } finally { //在finally中释放锁,确保锁的释放 lock.unlock(); } } } }
死锁:
概念:由于俩个或多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往CPU执行
建议:不要写锁的嵌套,防止死锁发生