l 多线程概述
日常生活中很多事都是可以同时进行的,例如:人可以同时进行呼吸、血液循环、思考问题等活动。
多线程是指一个应用程序中有多条并发执行的线索,每条线索都被称作为一个线程,它们会交替执行,彼此之间可以进行通信。
多线程之主线程概念:JVM启动后,会有一个执行路径(线程)从main方法开始的,一直执行到main方法结束,这个线程在Java中称之为主线程。
线程对象调用 run方法和调用start方法区别?
调用run方法不开启线程。仅是对象调用方法。
调用start开启线程,并让jvm调用run方法在开启的线程中执行。
Thread类
创建线程
方式一继承Thread类
创建线程的步骤
- 定义一个类继承Thread类。
- 重写run方法。
- 创建子类对象
调用start方法,开启线程并让线程执行,同时还会告诉JVM去调用run方法。
public class Demo { public static void main(String[] args) { //创建自定义线程对象 MyThread myThread = new MyThread(); //开启新线程 myThread.start(); while (true) {// 死循环 System.out.println("Main 方法在运行"); } } }
方式二 实现Runnable接口
- 定义类实现Runnable接口
- 覆盖接口中的run方法
- 创建Thread类的对象
- 将Runnable接口的子类对象作为参数传递给Thread类的构造函数
- 调用Thread类的start方法开启线程
方式三实现callable接口:此方法不常用就不做探究了,还有别的开启线程的方法,我最常用的是上面俩种。
线程安全问题
个人理解:线程安全就是说多线程访问同一代码,不会产生不确定的结果,而不安全就是多个线程对同一个对象进行操作,对象本身会产生不确定的结果。
举个例子:你去食堂打饭(你是一个线程),你来晚了,就剩最后一份糖醋排骨了(不纠结喜不喜欢吃的问题)你正在向阿姨要最后一份排骨,这时候另外一个人(另外一个线程)过来插你队也要糖醋排骨。这时候阿姨按理来说应该把排骨给你,可是这个插你队的是阿姨侄子,阿姨可以给你也可以给他侄子(就看公私分不分明)。这样阿姨就处在不确定的情况下。而线程安全就是直接强行规定死了,只能你这个线程打完饭,后面的人才能去打饭,只有等你释放了食堂阿姨这个资源之后下面的人才能去进行(打饭)操作。你不打完饭释放资源,下面的不可能去操作食堂阿姨这个角色。(个人理解有问题欢迎━(*`∀´*)ノ亻!指正)
不喜欢吃糖醋排骨我再来一个:买票
多线程模拟火车站的售票窗口,每一个线程表示一个售票窗口,共出售100张票
ublic class Demo2 { public static void main(String[] args) { TicketWindow window = new TicketWindow(); new Thread(window,"窗口1").start(); new Thread(window,"窗口2").start(); new Thread(window,"窗口3").start(); new Thread(window,"窗口4").start(); } } class TicketWindow implements Runnable{ private int tickets = 100; @Override public void run() { while(true){ if(tickets > 0){ try { //睡眠让问题更容易“暴露” Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } String threadName = Thread.currentThread().getName(); System.out.println(threadName + "正在发售第"+tickets--+"张票"); }else{ break; } } } }
上面的例子,由于多个线程共用了同一个资源(票),会造成线程安全问题,票售出了-1张
错误截图
线程安全问题都是由全局变量、静态变量以及共享的变量引起的。若每个线程中对全局变量、静态变量以及共享的变量只有读操作,那么这个全局变量是线程安全的;
若有多个线程同时执行写操作(修改),都需要考虑线程同步,否则就可能发生线程安全问题。
Java中提供了线程同步机制,它能够解决上述的线程安全问题。
方式一:同步代码块
方式二:同步方法
同步代码块: 在代码块声明上,加上synchronized
synchronized (锁对象(lock、被锁对象)) { 可能会产生线程安全问题的代码 }
同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同一个锁对象才能够保证线程安全。
lock 是一个锁对象,是同步代码块的关键,锁对象可以是任意类型的对象,是多个线程共享的锁对象 它必须是唯一的,“任意”说的是共享锁对象的类型。所以,锁对象的创建代码不能放到run()方法中,否则每个线程都会创建一个新的对象,每个锁都有自己的标志,那就没有意义了
被锁对象:表示如果当前线程访问"被锁对象"的synchronized的代码块时,其它线程不能访问此代码块,另外,也不能访问"被锁对象"中的其它synchronized的代码块;
同步的好处:解决了多线程并发访问的问题,使多线程并发访问的共享数据能够保持一致性
同步的弊端:使当前的对象的工作效率降低;因为要处理线程同步的问题;
同步方法:在方法声明上加上synchronized
public synchronized void method(){ 可能会产生线程安全问题的代码 }
同步方法的也是一样锁住同步的代码,但是锁对象的是Runable实现类对象,也就是this,谁调用方法,就是谁,在这里就是创建的run对象
第三种方法:Lock接口
java.util.concurrent.locks.Lock
Lock接口中的方法:
void lock():获取锁
void unlock():释放锁
使用方法:
1、在Runable实现类的成员变量创建一个ReentrantLock对象
2、在可能产生线程安全问题的代码前该对象调用lock方法获取锁
3、在可能产生线程安全问题的代码后该对象调用unlock方法获取锁
public class xiancheng implements Runnable { int ticket = 100;//火车票100张 Object o = new Object();//创建一个锁对象 ReentrantLock r = new ReentrantLock(); // java除了使用关键字synchronized外,还可以使用ReentrantLock实现独占锁的功能。 // 而且ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景 // 在Runable实现类的成员变量创建一个ReentrantLock对象 @Override public void run() { while (true) { if (ticket > 0) { try { r.lock();//在可能出现线程安全的地方上锁 Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } finally { // unlock方法放在finally里面,无论程序是否有出现异常,该方法都会执行,也就是都会释放锁 r.unlock();// 3、在可能产生线程安全问题的代码后该对象调用unlock方法获取锁 } } } } }