1.死锁
死锁是指多个线程或进程在访问共享资源时,由于彼此占用的资源被其它线程或进程占用而互相等待,导致所有线程或进程都无法继续执行的状态。
实例操作:
创建类,并创建两个静态对象变量
public class MySuo {
public static Object lockA = new Object();
public static Object lockB = new Object();
}
再创建两个类实现Runnable接口,并实现run方法添加自动锁调用上方类中的静态变量
public class HeSuo implements Runnable{
@Override
public void run() {
synchronized (MySuo.lockA){
System.out.println("获取到了锁A");
synchronized (MySuo.lockB){
System.out.println("获取到了锁B");
System.out.println("可以开门了");
}
}
}
}
public class SheSuo implements Runnable{
@Override
public void run() {
synchronized (MySuo.lockB){
System.out.println("获取到了锁B");
synchronized (MySuo.lockA){
System.out.println("获取到了锁A");
System.out.println("可以开门了");
}
}
}
}
两个线程类,第一个调用了A锁,第二个调用了B锁,此时,第一个线程执行完任务等待B锁的释放,但是第二个线程也执行完任务在等待A锁的释放,两个锁就在互相等待对方释放,就会出现死锁的情况
创建测试类测试
public class Test {
public static void main(String[] args) throws Exception{
new Thread(new HeSuo()).start();
new Thread(new SheSuo()).start();
}
}
可以看到输出结果,并没有输出“可以开门了”,锁到这里就死了。需要避免这种情况,可以使用Thread.sleep在两个线程中,让第二个线程等一等第一个线程
2.线程的创建
线程有三种创建方式,第一种,继承Thread类,第二种,实现Runnable接口,第三种实现Callable接口。三种创建方式都需要实现run方法。一下是三种创建方式的代码。
public class MyThread extends Thread{
@Override
public void run() {
super.run();
}
}
public class MyThread implements Runnable{
@Override
public void run() {
}
}
public class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return null;
}
}
三种方式各有优点,第一种更方便的创建以及启动,但是继承只能继承一个类,第二章,使用实现的方式,可以继承其他类,也可以继续实现其他接口,第三种是在第二种的方式上添加了返回值。
3.线程通信
线程通信是通过Object类中的wait()和notify()方法实现的,wait方法可以使当前执行该方法的线程进入等待状态,直到另一个线程调用该对象的notify()或notifyAll()方法才能唤醒该线程。
wait和sleep的区别:
父类:wait的父类是Object,sleep的父类是Thread
状态:wait执行时,线程是等待或者阻塞状态,sleep是休眠状态
锁不同:wait执行时,会释放锁资源,而sleep不会释放锁
案例实操:A负责在没钱的时候往银行卡存入钱,B负责在银行卡有钱的时候花掉钱
说明:案例中有存入和取出的方法,可以使用两个线程来表示。分别创建存入和去除的线程类并实现Runnable接口中的run方法。还有银行卡类,银行卡类中有金额变量,标记位(标记银行卡内是否余额),还有存入和取出的方法。
因为AB使用的是同一张银行卡,所以两个线程类需要调用同一个银行卡类。创建私有银行卡类变量,并添加构造函数,使两个线程类都调用传入进来的银行卡类。
public class SaveTask implements Runnable{
private BankRunnable bankRunnable;
public SaveTask(BankRunnable b){
bankRunnable = b;
}
@Override
public void run() {
}
}
public class TakeTask implements Runnable{
private BankRunnable bankRunnable;
public TakeTask(BankRunnable b){
bankRunnable = b;
}
@Override
public void run() {
}
}
银行卡类中创建金额变量、标记位以及存入取出方法
public class BankRunnable {
private static int money = 0;
private static boolean flag = false;
public synchronized void save(int num) throws Exception{
}
public synchronized void take(int num) throws Exception{
}
}
如果余额不为0的话标记为为true,反之false
存入金额需要先判断是否还有余额,如果有则不存入
if(flag){
this.wait();
}
在save方法内添加判断,如果为true则表示还有余额,使用该线程调用wait方法,使此线程进入等待,并释放锁资源,使得其他线程进入锁
如果没有余额则执行存入
money+=num;
System.out.println(Thread.currentThread().getName()+"存入"+num+",余额"+money);
flag = true;
notify();
将余额增加,并输出一下当前余额,将标记位改为true表示卡内有余额,并调用notify方法唤醒其他的线程进入方法锁。
取出方法则与存入方法相反
public synchronized void take(int num) throws Exception{
if(!flag){
this.wait();
}
money-=num;
System.out.println(Thread.currentThread().getName()+"存入"+num+",余额"+money);
flag = false;
notify();
}
如果为false表示没有余额了让此线程等待并释放锁资源,反之则让余额减少。
存入线程类中的run方法则是for循环执行银行卡中的存入方法
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
bankRunnable.save(1000);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
取出线程类中的run方法则是for循环执行银行卡中的取出方法
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
bankRunnable.take(1000);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
测试类,创建银行卡类,并将银行卡传入创建的线程类中
public class Test {
public static void main(String[] args) {
BankRunnable bankRunnable = new BankRunnable();
new Thread(new SaveTask(bankRunnable)).start();
new Thread(new TakeTask(bankRunnable)).start();
}
}
可以看到输出结果,有余额执行取出,没有余额执行存入
不仅notify方法可以唤醒线程,notifyAll方法也可以唤醒线程,他们两个的区别是:notify方法是唤醒的随机一个线程,而notifyAll方法唤醒的是全部线程。
4.线程状态
通过线程类中的getState方法来获取线程的状态,线程有6中状态
NEW:线程创建的状态
RUNNABLE:start()是就绪状态-时间片-运行状态,统称为RUNNABLE
WAITING:无期等待,调用wait方法会进入此状态
TIMED_WATING:有期等待,当调用sleep方法会进入此状态
TERMINATED:终止状态,代码任务执行完毕或程序遇到异常的状态
5.线程池
线程池有多种创建方法
第一种:固定长度的线程池
第二种:单一线程池
第三种:可变线程池
第四种:延迟线程池
public class TestPool {
public static void main(String[] args) {
//创建一个固定长度的线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);//传入的参数表示创建几个线程池
//创建单一的线程池,实现只有一个线程
ExecutorService executorService1 = Executors.newSingleThreadExecutor();
//创建可变线程池,线程池的数量是可以变换的
ExecutorService executorService2 = Executors.newCachedThreadPool();
//创建延迟线程池,以及5个固定长度的线程池
Executors.newScheduledThreadPool(5);
}
}
但上面都是通过Executors工具类创建的,但是阿里巴巴不建议使用,阿里建议使用原生的模式创建线程池。
ArrayBlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(5);//等待的队列最大数
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 10, TimeUnit.SECONDS, workQueue);
ThreadPoolExecutor 的传入五个参数分别是:核心线程池个数,最多线程个数,线程池空闲时间(空闲了多久撤掉此线程池),空闲时间的单位,等待队列
这样能更灵活的创建线程池
Executors工具类有多个方法可供使用
根接口:
void execute(Runnable command):执行Runnable类型的任务
子接口:
void shutdown():关闭线程池。需要等任务执行完毕。
shutdownNow(); 立即关闭线程池。 不在接受新的任务。
isShutdown(): 判断是否执行了关闭。
isTerminated(): 判断线程池是否终止。表示线程池中的任务都执行完毕,并且线程池关闭了
submit(Callable<T> task);提交任务,可以提交Callable
submit(Runnable task): 提交任务,可以提交Runnable任务
shutdown的操作
public class TestExecutors {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 20; i++) {
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
}
executorService.shutdown();
}
}
使用shutdown和shutdownNow的区别,看输出结果
左边使用了shutdown方法,右边使用了shutdownNow的方法,shutdown是等待程序执行完毕停止线程,而shutdownNow则是立即停止线程。
6.Callable创建线程
创建类并实现Callable接口传入泛型,需要什么类型返回值就传入什么类型。
public class MyCall implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 101; i++) {
sum+=i;
}
System.out.println("线程的结果:"+sum);
return sum;
}
}
随便返回一个数字,我这返回的是1-100相加。然后在测试类中测试。
public class MyCallTest {
public static void main(String[] args) throws Exception{
//如果使用普通的线程来执行线程类,则会比较的麻烦
MyCall myCall = new MyCall();
FutureTask<Integer> futureTask = new FutureTask<>(myCall);
new Thread(futureTask).start();
//如果使用线程池中的对象,则相对简单一些
Future<Integer> submit = Executors.newCachedThreadPool().submit(new MyCall());
System.out.println("返回的结果:"+ submit.get());
}
}
两种测试的方式,都可以得到结果,但相同第二种会比第一种简单一些,第一种使用的是线程来执行,第二种使用线程池中的对象来执行
可以看到,使用线程执行得不到值,使用线程池对象可以使用.get方法得到返回的值