多线程
真正的多线程是指有多个CPU,即多核处理器
- 线程就是独立的执行路径
- 在程序运行时,即使没有自己创建线程,后台也会存在多个线程,如gc线程、main主线程
- main()称之为主线程,为系统的入口点,用于执行整个程序
- 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统密切相关的,先后顺序是不能人为干预的
- 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
- 线程会带来额外的开销,如CPU调度时间,并发控制开销
- 每个线程在自己的工作内存交付,加载和存储主内存控制不当会造成数据不一致
线程更多概念方面的内容见《操作系统》这本教材
1 线程的创建
线程有四种创建方式:
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- 线程池(※※JUC的内容※※)
1.1 继承Thread类
- 继承Thread类
- 重写run方法
- 创建对象,
.start()
public class ThreadDemo extends Thread {
public ThreadDemo(String name) {
super(name);
}
@Override
public void run() {
String currName = Thread.currentThread().getName();
System.out.println(currName);
}
public static void main(String[] args) {
for (int i = 0; i < 100; ++i) {
// 启动线程,不保证立刻上CPU
new ThreadDemo("Thread" + i).start();
// 可能有人会纠结为什么不这样写………
// 这样只是调用类实例的方法,并非启动多线程
// new ThreadDemo("Thread" + i).run();
}
}
}
100个线程创建后的执行顺序存在随机性,无序性。
1.2 Runnable接口
- 实现Runnable接口
- 实现run方法
- 创建Thread实例,将Runnable接口传入,并调用start方法
- Runnable是函数式接口,推荐使用Lambda表达式简化代码
public class RunnableDemo {
public static void main(String[] args) {
new Thread(()->{
String name = Thread.currentThread().getName();
for (int i = 0; i < 10; i++) {
System.out.println(name + "---" + i);
}
}, "my Thread").start();
for (int i = 0; i < 10; i++) {
System.out.println("main--" + i);
}
}
}
两个线程:main线程和my Thread线程穿插执行
1.3 Callable接口
- 实现Callable接口,重写call方法
- 创建Thread对象,传递Callable接口的实现类,调用start方法
class MyThread implements Callable<Integer> {
@Override
@SneakyThrows
public Integer call(){
String tName = Thread.currentThread().getName();
System.out.println(tName + " coming callable");
TimeUnit.SECONDS.sleep(2);
return 1024;
}
}
public class CallableDemo {
public static void main(String[] args){
CallableDemo callableDemo = new CallableDemo();
callableDemo.demo3();
}
/**
* 将两个线程的计算结果求和
*/
@SneakyThrows
public void demo1() {
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread());
new Thread(futureTask, "t1").start();
int res1 = 10;
System.out.println(Thread.currentThread().getName() + "线程");
// 要求获得Callable线程的计算结果,
// 如果没有计算完成就获取,会导致阻塞,只能等到计算完成
// 除非必要,建议最后使用futureTask.get()
while(!futureTask.isDone());
int res2 = futureTask.get();
System.out.println(res1 + res2);
}
@SneakyThrows
public void demo2() {
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread());
// 多个线程使用同一个futureTask,只会调用一次call
// 同一个futureTask计算结果可以重复利用,不会被多次执行
new Thread(futureTask, "t1").start();
new Thread(futureTask, "t2").start();
}
@SneakyThrows
public void demo3() {
MyThread myThread = new MyThread();
FutureTask<Integer> futureTask = new FutureTask<>(myThread);
FutureTask<Integer> futureTask2 = new FutureTask<>(myThread);
// 多个线程使用同一个Callable实现类,只会调用一次call
new Thread(futureTask, "t1").start();
new Thread(futureTask2, "t2").start();
}
}
2 线程状态
3 线程同步
首先,为什么要线程同步?看下面经典的买票示例
public class TicketWindow{
// 买票窗口有票这个资源
private Ticket ticket = new Ticket();
// 卖票
public void sellTicket() {
String tName = Thread.currentThread().getName();
// 获取剩余的票量
int ticketNum = this.ticket.getTicketNum();
// 修改剩余的票量
int left = ticketNum - 1;
this.ticket.setTicketNum(left);
System.out.println(tName + "卖的票编号为" + ticketNum + ",当前剩余" + left);
}
public boolean isEmpty() {return ticket.getTicketNum() <= 0;}
public static void main(String[] args) {
TicketWindow ticketWindow = new TicketWindow();
for(int i = 1 ; i <= 50; ++i) {
String tName = "售票员" + i;
new Thread(()->{
while(!ticketWindow.isEmpty()) {
ticketWindow.sellTicket();
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},tName).start();
}
}
}
// 资源类
class Ticket {
@Getter
@Setter
private int ticketNum = 100;
}
如图所示的问题,一张票被多个售票员卖出,显然是不合理的,为了解决这个问题,引入线程同步。
3.1 同步方法
修改上面的案例,将操作资源类ticket
的方法sellTicket
修改为同步方法
public synchronized void sellTicket() {
……
}
3.2 同步代码块
同步块:synchronized(obj) {} ,obj称为同步监视器
-
obj可以为任何对象,但是推荐使用共享资源作为同步监视器,
-
同步方法中无需指定同步监视器,因为同步方法的同步监视器是this即该对象本身,或class即类的模子
同步监视器的执行过程
- 第一个线程访问:锁定同步监视器,执行其中代码
- 第二个线程执行,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕后,解锁同步监视器
- 第二个线程访问,发现同步监视器未锁定,锁定并访问
public void sellTicket() {
synchronized (ticket) {
String tName = Thread.currentThread().getName();
// 获取剩余的票量
int ticketNum = this.ticket.getTicketNum();
// 修改剩余的票量
int left = ticketNum - 1;
this.ticket.setTicketNum(left);
System.out.println(tName + "卖的票编号为" + ticketNum + ",当前剩余" + left);
}
}
3.3 Lock锁(※JUC内容※)
4 线程的其他方法
方法 | 说明 |
---|---|
sleep() | ①使线程停止运行一段时间,将处于阻塞状态 ②如果调用了sleep方法后,没有其他等待执行的线程,这个时候当前线程不会马上恢复执行 |
join() | 阻塞指定线程等到另一个线程完成以后再继续执行 |
yield() | ①让当前正在执行的线程暂停,不是阻塞线程,而是将线程转入就绪状态 ②调用了yield方法后,如果没有其他等待执行的线程,此时当前线程就会马上恢复执行 |
setDaemon() | ①可以将指定的线程设置为后台线程,守护线程 ②创建用户线程的线程结束后,后台线程也随之消亡 ③只能在线程启动之前把他设为后台线程 |
setPriority(int newPriority) | ①线程的优先级代表的是概率 ②范围从1—10,默认5 |
getPriority() | 获取优先级 |
stop() | 停止线程,不推荐使用 |
isAlive() | 判断线程是否还活着 |
setName() | 给线程起一个名字 |
getName() | 获取线程的名字 |
currentThread() | 取得当前正在运行的线程对象,也就是获取自己本身 |