1. 多线程案例
- 这是一个经典的多线程案例,火车票售票案例,
- 需求:假设一共100张火车票,多个窗口去售卖 。
- 想一想,在现实中是如果买票的,需要注意哪些事项 ?
1.1 窗口售票
1.1.1 方式一:继承 Thread 类
- 第一种继承
Thread
方式看看有什么效果!? 会出现什么样的问题!?
public class Test_Ticket {
public static void main(String[] args) {
TicketThread t = new TicketThread();
TicketThread t1 = new TicketThread();
TicketThread t2 = new TicketThread(); //在sleep()时增加一个窗口。
t.start();
t1.start();
t2.start();//增加售卖窗口开始买票
}
}
/*
自定义类
*/
class TicketThread extends Thread{
//int num_ticket =100;//1.定义总票数;
static int num_ticket =100;//使用static 全局变量定义总票数;
//重写run() 方法
@Override
public void run() {
//设置循环保证有票就要卖
while (true){
//2.TODO 注意:如果让线程休眠了(其目的就是为了让线程出错,增加了线程切换频率)。还能保证正确性么?
try {
Thread.sleep(10);//单位毫秒。
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印卖票信息
System.out.println(Thread.currentThread().getName()+" : "+num_ticket--);
if (num_ticket<=0)break;//设置循环出口当没有票时就跳出。
}
//窗口提示消息
if (num_ticket==0){
System.out.println("票已经售完");
}
}
}
- 经过休眠之后产生的问题:
-
产生第一个问题:出现了重复买票现象;
-
产生的第二个问题:出现了超卖的现象;
-
1.1.2 方式二:实现Runnable接口
- 实现接口能解决相应问题么!?
//业务场景同Thread一样。
public class Test_Ticket2 {
public static void main(String[] args) {
MyRunnable r = new MyRunnable();
Thread t = new Thread(r);
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
Thread t3 = new Thread(r);
t.start();
t1.start();
t2.start();
t3.start();
}
}
//自定义类实现接口
class MyRunnable implements Runnable{
int num_Ticket =100;
//重写run()方法
@Override
public void run() {
//循环一直卖票
while (true){
if (num_Ticket>0) { //假设我让票大于0在开始卖呢!?还会发生那样的问题么!?
//TODO :制造睡眠,让线程产生频换切换,出现错误。
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印 输出当前线程名称 + 票号
System.out.println(Thread.currentThread().getName() + "= " + num_Ticket--);
}
//循环出口,当没有余票时,停止。
if (num_Ticket<=0) break;
}
}
}
1.2 售票案例—同步锁
- 我们在上述 “售票案例” 中发现了多线程由于线程睡眠(产生现象线程的频换切换)导致了出现了问题: 超卖 和 重卖。
- 如何判断线程中有没有安全问题!?参考以下3点:
- 这个程序时不是多线程!?
- 在代码执行中有没有共享数据!?
- 有没有多条语句去操作共享数据!?
- 以上的问题用什么解决方式: 到底该从哪方面入手呢!? 能不能把有问题的代码上一把锁,谁用谁开!?
- 解决问题,就需要把有问题的代码,“全部包裹起来”,一次只让一个线程去执行,给上一把锁。
- 让多线程对操作的共享数据,做到一个排队现象。如生活中的案例: 上厕所。
1.2.1 Synchronized
-
就是使用
Synchronized
将会引起线程安全问题的逻辑代码,用锁的形式,“锁起来”,只有持有这个锁的钥匙才能访问,并且,同一时刻,只有这一个线程持有这把锁的钥匙,从而确保线程安全。- 同步需要两个或者两个以上的线程。
- 多个线程之间必须使用同一个锁。
sychronized(锁对象){ // 这里的对象,相当于多线程的锁。线程相当于从该对象拿到了一个锁。 容易出问题的代码(需要共享的代码,涉及到共享操作的) }
-
使用位置,锁的对象必须是唯一
- 可以修饰 方法称为同步方法,会自动分配,使用的锁对象是
this
。 - 可以修饰代码块称为同步代码块,锁对象可以任意,但是必须唯一。
- 可以修饰 方法称为同步方法,会自动分配,使用的锁对象是
-
使用同步锁的特点:
- 同步的缺点是会降低程序的执行效率, 对执行流程可控,但是为了保证线程安全,必须牺牲性能。
- 要控制锁的范围。不需要给所有代码都上锁,例如:去卫生间给整个商场都上锁,就是不对滴。
1.3 使用同步锁改造售票案例
1.3.1 改造Thread 和 Runnable
- 锁的位置需要考虑,小了控制不住,大了又牺牲了性能。
- 如何判断线程中有没有安全问题!?
public class Test_Ticket {
public static void main(String[] args) {
//创建线程
MyThread t = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
MyThread t4 = new MyThread();
t.start();
t2.start();
t3.start();
t4.start();
}
}
class MyThread extends Thread{
static int num_Ticket=100; //票数(共享数据)
//static Object o = new Object(); 这个也可以 也是全局唯一。
Object o = new Object(); //创建唯一对象。
/*1.确定多线程隐患:3点: 是不是存在多个线程中? 存在共享数据!? 是否有多条语句操作该共享数据!?
2.确定范围:范围太大,影响性能,范围太小,没有作用。
*/
//重写run()方法
@Override
public void run() {
while (true) {
//TODO 制造困难 睡上一会。
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//synchronized (new Object()){ // new Object()也是任意对象。 运行结果!? 没用!为什么?
//synchronized (o){ // 不管你怎么调用我的o对象引用只有一次。 运行结果!? 还不行!? Why!?
synchronized (MyThread.class){ //使用本类。类对象锁定!!相当于锁住了本类。 运行结果: ok
//TODO 售票的业务处理
if (num_Ticket > 0) {
System.out.println(getName() + " = " + num_Ticket--);
}
if (num_Ticket <= 0)break;//break,只能在循环里使用。
}
}
}
}
- 改造Runnable
public class Test_Runnable {
public static void main(String[] args) {
MyRunnable r = new MyRunnable();
Thread t = new Thread(r);
Thread t2 = new Thread(r);
Thread t3 = new Thread(r);
Thread t4= new Thread(r);
t.start();
t2.start();
t3.start();
t4.start();
}
}
class MyRunnable implements Runnable{
int num_Ticket =100; //票总数
Object o = new Object();
@Override
//synchronized public void run() { 可以直接在方法上加锁
public void run() {
//TODO 制造困难 睡觉10
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
while (true) {
// synchroized(new Object()) // 对象不唯一。
synchronized (this) { //使用本类对象,指的是this 或者 // synchronized(o)也行
if (num_Ticket > 0) {
System.out.println(Thread.currentThread().getName() + "=" + num_Ticket--);
}
if (num_Ticket <= 0) break;//循环出口。
}
}
}
}
- 还记得之前学过的StringBuffer 和StringBuilder:有看过它们底层实现么!!!?
2. 线程池
2.1 概述
- “池化技术的思想” 主要是为了减少每次获取资源的消耗,提高对资源的利用率。其目的是限制和管理资源,如:使用线程池可以进行统一的分配,调优和监控。
- 降低资源消耗。
- 提高相应速度。
- 提高线程的可管理性。
2.2 线程池创建方式
2.2.1 ExecutorService/Executors
- ExecutorService 属于接口,用来存储线程的池子,把新建线程/启动线程/关闭线程的任务都交给池来管理。
execute(Runnable任务对象) 把任务丢到线程池。
- Executors属于类,提供了工厂方法用来创建不同类型的线程池。
newFixedThreadPool(int nThreads) 最多n个线程的线程池 newCachedThreadPool() 足够多的线程,使任务不必等待 newSingleThreadExecutor() 只有一个线程的线程池
2.2.2 售票案例:用线程池创建线程
-
通过线程池技术,改造售票案例 Runnable 接口。
public class Test_Runnable { public static void main(String[] args) { MyRunnable r = new MyRunnable(); // 使用线程池改造 /* Thread t = new Thread(r); Thread t2 = new Thread(r); Thread t3 = new Thread(r); Thread t4= new Thread(r); t.start(); t2.start(); t3.start(); t4.start(); */ //Todo 不想那么麻烦使用Thread 的start()方法去启动线程。 使用线程池技术来管理资源的启动。 ExecutorService pool = Executors.newFixedThreadPool(3);//相当于启动3个线程。 // pool.execute(r); //这只是取了一根线程。多线程怎么使用!? 通过循环。 // 池子里有3个但是循环取了5个!? 相当于3个线程循环执行,提高使用率。如果循环少于线程数,相当于闲着线程。 for (int i = 0; i <6 ; i++) { pool.execute(r); } } } class MyRunnable implements Runnable{ int num_Ticket =100; //票总数 Object o = new Object(); @Override //synchronized public void run() { 可以直接在方法上加锁 public void run() { //TODO 制造困难 睡觉10 try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } while (true) { synchronized (this) { //使用本类对象,指的是this 或者 // synchronized(o)也行 if (num_Ticket > 0) { System.out.println(Thread.currentThread().getName() + "=" + num_Ticket--); } if (num_Ticket <= 0) break;//循环出口。 } } } }
3. 线程锁
3.1 悲观锁和乐观锁
- 悲观锁:总是假设最坏的情况出现,用悲观的态度防止 并发间线程出现的冲突。
- 在进行读写操作时,在释放锁之前,任何人都不能对其数据进行操作,直到前面释放锁之后,后一个对数据加锁在进行操作。同
sychronized
一样。
- 在进行读写操作时,在释放锁之前,任何人都不能对其数据进行操作,直到前面释放锁之后,后一个对数据加锁在进行操作。同
- 乐观锁:总是朝着最好的结果看待事情,理想的认为每次数据不会被修改,所以不需要持锁操作。
- 但是在更新的时候会通过版本号机制或者算法(CAS,Compare And Swap),去判断一下是否发生过数据更改。
3.2 常见的锁(拓展知识)
-
sychronized
互斥锁, (悲观锁)- 采用synchronized修饰符实现的同步机制叫做互斥锁机制。每个对象都有一个monitor(锁标记),当线程拥有这个锁标记时才能访问这个资源。如果没有这个锁标记,任何一个对象系统都会为其创建一个互斥锁,这个锁是为了分配给线程的,防止打断原子操作。每个对象的锁只能分配给一个线程,因此叫做互斥锁。
-
ReentrantLock
排他锁(悲观锁)- ReentrantLock是排他锁,又称独占锁。排他锁在同一时刻仅有一个线程可以进行访问,实际上是一种相对比较保守的锁策略。
- 在这种情况下任何“读/读”、“读/写”、“写/写”操作都不能同时发生,这在一定程度上降低了吞吐量。例子:若操作者S 对数据对象A加上独占锁,则只允许操作者S读取和修改A,其他任何事务都不能再对A加任何类型的锁,直到释S放A上的锁。这就保证了S在释放A上的锁之前不能再读取和修改A。(然而读操作之间不存在数据竞争问题,如果”读/读”操作能够以共享锁的方式进行,那会进一步提升性能。) 所以需要读写锁。
-
ReentrantReadWriteLock
读写锁(乐观锁)- 因此引入了
ReentrantReadWriteLock
,顾名思义,ReentrantReadWriteLock是Reentrant(可重入,可重新在进入的意思)Read(读)Write(写)Lock(锁),我们下面称它为读写锁。读写锁内部又分为读锁和写锁, 读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。 - 读锁和写锁分离从而提升程序性能,读写锁主要应用于读多写少的场景。
- 因此引入了
3.3 用读写锁改造售票案例
-
同悲观锁
sychronized,
属于jvm底层级别,将读写都锁住了,其实多线程的 读 并不影响数据的完整性,主要是修改。所以使用ReentrantReadWriteLock
将写的过程控制,适用于读多写少的场景。 -
ReentrantReadWriteLock
分公平锁和非公平锁,公平锁即,排队顺序拿锁,先排先得,非公平不看排队顺序,可以插队。- 非公平锁性能比公平锁高5~10倍, 因为公平锁需要频繁唤醒队列中的线程,比较消耗资源。但是非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
public class Test_Reentrant {
public static void main(String[] args) {
MyRunnable_Ticket r = new MyRunnable_Ticket();
Thread t = new Thread(r);
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t.start();
t1.start();
t2.start();
}
}
class MyRunnable_Ticket implements Runnable{
int num_ticket =100; //总票数
//1. 创建读写锁,static 修饰保证全局唯一
static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);//默认false 非公平模式,容易死锁,性能高。
@Override
public void run() {
while (true) {
/*
2. 在操作资源前面上锁
*/
lock.writeLock().lock();//上了写锁
// Todo 制造睡眠,切换线程
try {
Thread.sleep(10);
// 售票
if (num_ticket > 0) {
System.out.println(Thread.currentThread().getName() + "=" + num_ticket--);
}
//不满足条件就跳出while循环
if (num_ticket <= 0) break;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.writeLock().unlock();//3. 不会像sychronized自动放锁,它需要手动释放。
}
}
}
}
- 拓展知识:
sychronized
也属于非公平锁,因为它获取锁的方式也是随机不排队的。- 二者区别在于:
synchronized
在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象, 因此使用 Lock 时需要在finally
块中释放锁。
- 二者区别在于:
4. 设计模式
4.1 怎么理解设计模式!?
- 设计模式是 针对面向对象中反复出现问题的解决方案,是一套反复被开发者使用,并且多数人知晓的,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用。 设计模式一共有23种之多,都是针对不同场景的解决方案。
4.2 单例设计模式(Singleton)
- 单例设计(singleton):一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。它最主要的特点就是保证在内存中该类只有一个实例对象。 这样做的好处,反复创建对象或者销毁,引起的资源浪费。
- 我们先看一个典型的单例模式设计的类
java.lang.Runtime
,分析下单例如何创建!?
4.2.1 单例模式一:饿汉式
- 啥是饿汉式!?是一种设计思想,举一个生活例子:一个很饿的人,它很勤快就怕自己饿着,提前准备食物,什么时候想吃,拿过来就吃。
- 即饿汉式在 开始类加载的时候就已经实例化,并且创建单例对象,用的时候拿过来就用。
/**
饿汉式
*/
public class Test_Singleton {
public static void main(String[] args) {
//4.测试
MySingLeton s = MySingLeton.getSingLeton();
System.out.println(s);
//4.1 怎么测试创建成功,地址值相同即可! s==s1。
MySingLeton s1 = MySingLeton.getSingLeton();
System.out.println(s1);
}
}
class MySingLeton{
//1.私有化 无参构造方法,无参通过构造方法创建对象。
private MySingLeton(){}
//2.在类的内部,创建本类对象,私有化该对象
//2.1 当提供的对外访问方法用static 修饰后 创建本类对象也需要用static修饰,静态只能调用静态
// private MySingLeton singLeton = new MySingLeton();
static private MySingLeton singLeton = new MySingLeton();
//3.提供对外访问方法。
//3.1 无法创建对象,怎么访问该方法!? 所以需要static 修饰,通过类名.方法调用!
// public MySingLeton getSingLeton() {
static public MySingLeton getSingLeton() {
return singLeton; // 为什么!?会报错,因为静态只能调用静态
}
}
4.2.2 单例模式二: 懒汉式(面试知识点)
- 啥是懒汉式!?生活中的例子:因为它比较懒么!?什么时候饿了,什么时候在想办法搞点食物!
- 即懒汉式: 开始不会实例化,什么时候用就什么时候new,才进行实例化 需要时在使用,相当于 延迟加载。
/**
* 懒汉式:面试重点知识,同饿汉式创建方式大体相似,但是有细微不同!
* 设计到两个层面,1,什么是延迟加载!?
* 2,如何解决线程安全!?在多线程的情况下。
*
*/
public class Test_SingLeton2 {
public static void main(String[] args) {
MySingLeton2 mySingLeton = MySingLeton2.getMySingLeton();
System.out.println(mySingLeton);
MySingLeton2 mySingLeton2 = MySingLeton2.getMySingLeton();
System.out.println(mySingLeton2==mySingLeton);//判断是否一致。
}
}
class MySingLeton2{
static Object obj = new Object();
//1.私有化无参构造方法,目的:防止任意创建对象。
private MySingLeton2(){}
//2.在本类中,创建本类的对象。并私有化。
static private MySingLeton2 mySingLeton ; //2.1懒汉式,相当于什么时候需要什么时候在创建!--延迟加载!
//3.对外提供访问方法
static public MySingLeton2 getMySingLeton(){
//4.关键点,判断什么时候需要在创建!
//4.1 问题:该模式存在多个数据操作! 存在线程安全隐患!?怎么解决!?
synchronized (obj) { // 这样相当于将整个内容全部包裹进来!也可以是用类锁
if (mySingLeton == null) {
mySingLeton = new MySingLeton2();
}
return mySingLeton;
}
}
}
4.2.3 单例模式:懒汉式和饿汉式的区别
- 线程安全方面:
- 饿汉式: 线程不存在多数据操作,线程安全。
- 懒汉式:线程不安全,需要锁机制处理。
- 效率和内存空间:
- 饿汉式: 因为没有锁机制, 执行效率高,但是一开始就会加载对象,不管是否使用都会占用内存。
- 懒汉式:有锁机制,效率比饿汉式差点,但是不占用内存空间,因为什么时候使用什么时候创建。