创建线程的三种方式
1、继承Thread类
2、实现Runnable
3、实现Callable
继承Thread
public class MyThread extends Thread {
@Override
public void run() {
for(int i =0; i<=10;i++){
System.out.println("Thread线程执行"+i+"次:时间"+ LocalTime.now());
}
}
}
实现Runnable
public class MyRunnable implements Runnable {
@Override
public void run() {
for(int i =0; i<=10;i++){
System.out.println("Runable线程执行"+i+"次:时间"+ LocalTime.now());
}
}
}
实现Callable
public String call() throws Exception {
for(int i =0 ;i<=10;i++){
System.out.println("Callable主线程执行"+i+"次:时间"+ LocalTime.now());
}
return "完成";
}
主线程main
public class ThreadCreateDemo {
public static void main(String[] args) {
// 1、创建自定义线程类实例
// 2、启动线程
// 3、在main线程打印信息
// 一、Thread线程
// MyThread myThread =new MyThread();
// myThread.run();
// 二、Runnable线程
// Thread thread=new Thread(new MyRunnable());
// thread.start();
// 三、Callble线程
FutureTask<String> futureTask =new FutureTask<>(new MyCallable());
Thread thread =new Thread(futureTask,"MyCallable");
thread.start();
try {
System.out.println("Callable结果为:"+futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
for(int i =0 ;i<=10;i++){
System.out.println("主线程执行"+i+"次:时间"+ LocalTime.now());
}
}
}
线程的生命周期
image.png新建
new关键字创建了一个线程之后,该线程处于新建状态
jvm为线程分配内存,初始化成员变量
就绪
当线程对象调用了start()方法之后,该线程处于就绪状态
jvm为线程创建方法栈和程序计数器,等待线程调度器调度
运行
就绪状态的线程获取cpu资源,开始运行run()方法,该线程处于运行状态
阻塞
当发生如下情况时,线程会进入阻塞状态
- 线程调用sleep()方法主动放弃所占用的处理器资源
- 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
- 线程在等待某个通知(notify)
- 线程视图获得一个同步锁(同步监视器),但该同步锁正在被其它线程所持有的
- 程序调用了线程的suspend()方法将该线程挂起,但这个方法容易导致死锁,所以应该尽量避免发方法
死亡
线程会在以下情况时,被处于死亡状态
- run()或call()方法执行完成时,线程正常结束
- 线程跑出一个未补货的Exception或Error
- 调用该线程的stop()方法结束该线程,该方法容易导致死锁,不推荐使用
线程池Executor
创建和维护线程非常耗时,在企业开发中常见的办法是创建线程池,线程的创建和维护交给线程池处理。
image.png
public class ExecutorsCreateDemo {
//使用Executors获取线程对象
//通过线程池对象获取线程,并执行myRunnable实例
//分别打印线程池和主线程输出
public static void main(String[] args) {
// 创建并设置线程池的大小
ExecutorService executorService =Executors.newFixedThreadPool(5);
// 执行
executorService.execute(new MyRunnable());
for(int i =0 ;i<=10;i++){
System.out.println("主线程执行"+i+"次:时间"+ LocalTime.now());
}
}
}
线程安全
如果多个线程同时运行同一个实现了Runnable接口的类,程序每次运行结果和单线程运行结果是一样的,二其他的变量的值也和预期一致,就是线程安全;反之,线程不安全。
单线程卖火车票
火车票对象
public class Ticket implements Runnable {
// 假设票有100张
private int ticketNum = 100;
@Override
public void run() {
while (true) {
if (ticketNum > 0) {
// 有票,让线程睡眠100毫秒
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印当前售出的票数和线程名
String threadName = Thread.currentThread().getName();
System.out.println("线程" + threadName + "当前剩余火车票" + ticketNum-- + "张");
}
}
}
}
卖票窗口(一个线程一个窗口)
public class ThreadSafeDemo {
public static void main(String[] args) {
// 创建电影票对象
Ticket ticket = new Ticket();
// 创建一个线程:售票窗口
Thread thread = new Thread(ticket, "售票口1");
thread.start();
}
}
一个线程时,观察结果没有任何问题!
对上面代码进行修改,增加一个买票窗口,再观察结果!
加入了一个买票窗口
public class ThreadSafeDemo {
public static void main(String[] args) {
// 创建电影票对象
Ticket ticket = new Ticket();
// 创建一个线程:售票窗口
Thread thread1 = new Thread(ticket, "售票口1");
thread1.start();
Thread thread2 = new Thread(ticket, "售票口2");
thread2.start();
Thread thread3 = new Thread(ticket, "售票口3");
thread3.start();
}
}
image.png
- 此时出现了线程不安全的情况
当线程thread(窗口1)抢占到了cpu的执行权后并调用了sleep()方法后,线程会处于阻塞状态,ticketNum没有进行自剪操作,当窗口2拿到的票数还是之前的,就造成了两个窗口打印的票数一样的结果。
问题分析
线程安全问题都是由全局变量及静态变量引起的。
若每个线程对全局变量、静态变量只读不写,一般来说,这个变量线程安全。
若多个线程同时执行写的操作,一般都需要考虑线程同步,否则可能会影响线程安全。
解决方法-线程同步
要解决以上问题,只要在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺cpu资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的对象。
java引用了7中线程同步机制。
- 同步代码块(synchronized)
- 同步方法(synchronized)
- 同步锁(ReenreantLock)
- 特殊域变量(volatile)
- 局部变量(ThreadLocal)
- 阻塞队列(LinkedBlockingQueue)
- 原子变量(Atomic*)
加锁synchronized:在同步代码块或同步方法上加锁
public class Ticket implements Runnable {
// 假设票有100张
private int ticketNum = 100;
private Object lock = new Object();
@Override
public void run() {
while (true) {
// synchronized (lock) {
// if (ticketNum > 0) {
//// 有票,让线程睡眠100毫秒
// try {
// Thread.sleep(100);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
//// 打印当前售出的票数和线程名
// String threadName = Thread.currentThread().getName();
// System.out.println("线程" + threadName + "当前剩余火车票" + ticketNum + "张");
// ticketNum--;
// }
// }
saleTickt();
}
}
synchronized void saleTickt() {
// 当该方法是非静态方法,synchronized锁的是调用该方法的对象
// 当该方法是static,synchronized锁的是当前类的字节码对象。synchronized(Ticket.class)
if (ticketNum > 0) {
// 有票,让线程睡眠100毫秒
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印当前售出的票数和线程名
String threadName = Thread.currentThread().getName();
System.out.println("线程" + threadName + "当前剩余火车票" + ticketNum + "张");
ticketNum--;
}
}
}
结果:全是窗口1在卖票
上面方法看不到加锁的过程,所以有了同步锁(有好多实现类)
同步锁
public class Ticket implements Runnable {
// 假设票有100张
private int ticketNum = 100;
// 重入锁
// true:公平锁(每个线程都拥有拿到这个线程的权利);false:独占锁(只有某个线程执行完毕后才释放该锁)。独占锁相当于synchronized
private Lock locked = new ReentrantLock(true);
@Override
public void run() {
while (true) {
// 加锁
locked.lock();
try {
if (ticketNum > 0) {
// 有票,让线程睡眠100毫秒
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印当前售出的票数和线程名
String threadName = Thread.currentThread().getName();
System.out.println("线程" + threadName + "当前剩余火车票" + ticketNum + "张");
ticketNum--;
}
} catch (Exception e) {
} finally {
// 解锁(必须要解锁,否则会出现死锁)
locked.unlock();
}
}
}
}
image.png
synchronized和Lock区别
synchronized是jvm内置关键字,在jvm层面,Lock是java的一个类。
synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁
synchronized会自动释放锁(线程一执行完同步代码会释放锁,线程二在执行过程中发生异常会释放锁),Lock需在finally中手动释放锁(调用unlock()方法),否则容易发生死锁
用synchronized关键字的两个线程1和2,如果当线程1获得锁,线程2等待。如果线程1阻塞,线程2则会一致等待下去;而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了。
synchronized的锁可重入,不可中断、非公平,而LOCK锁可重入,可判断、公平和独占都可设置
Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码量少的同步问题
死锁的产生条件
互斥条件
进程要求对所有分配的资源(如打印机)进行排他性空值,即在一段时间内某资源仅为一个进程所战友。此时若有其他进程请求该资源,则请求进程只能等待
不可剥夺条件
进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己释放(只能是主动释放)
请求与保持条件
进程已经保持了至少一个资源,但又提出新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已经获得的资源保持不放
循环等待条件
存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求,即存在一个处于等待状态得集合(循环)。
网友评论