开启线程
普通开启线程
- 继承Thread:即是一个线程,又指定了线程要做的事情
- 实现Runnable:指定线程要做的事情,线程由Thread来做
其他开启线程的格式
-
匿名对象( 匿名对象: 适用于只使用一次的场景)
-
匿名内部类
实现Runnable接口的好处
- 1.由于Runnable的实现了对象只需创建一次,所以 共享数据不需要加static
-
- 实现接口的方式,避免了单继承的局限性
-
- 实现接口的方式,完成解耦的操作
-
- 线程池中只能使用Runnbale
Thread中的方法和注意
- 注意static关键词:
如果成员变量没有加static关键词,成员变量会在创建对象的时候初始化,每次创建对象, 都会初始化一个新的num, 所以会出现两条线程有自己的num
如果给num加上static修饰, 这个num就被该类的所有对象所共享
- 常用方法:
同步代码块
格式
- synchronized(锁对象) {
同步的代码
}
作用
- 同步代码块中的内容, 在执行的时候不会被抢走执行权
注意事项
- 锁可以是任意对象
- 多条线程想保证同步, 就必须使用同一把锁, 同一个锁对象
常用的锁对象
- this(可以使用, 但是需要保证只有一个本类对象)
- 类名.class(只有一个, 永远不会出问题)
synchronized工作流程
其他同步方法
同步方法
- 1.方法使用synchronized关键词修饰
- 2.普通同步方法,默认锁对象是:this
- 3.静态同步方法,默认锁对象:当前类名.class
(静态属于类, 它会优先于对象存在 -> 静态方法中没有this)
Lock锁(两个方法分别方法要同步代码的开头和结尾)
线程安全问题
产生的原因:
-
- 在多线程环境下
-
- 有共享数据
-
- 有多条语句操作共享数据
解决:
- 把所有操作共享数据的代码, 都放到同步中
多线程卖票案例
- Thread方式
/*
问题一: 不能使用while(true) 需要根据票数加条件
问题二: 由于tickets不是共享数据, 所以卖了300张票 将tickets变成共享数据
问题三: 两条线程都卖了第1张票 -> 线程安全问题
问题四: 没有将while(tickets > 0)放到同步代码块中, 所以出线程安全问题 while(tickets > 0)放在同步的里面
问题五: 将while(tickets > 0)放到同步代码块中, 一条线程把所有票都卖了, 解决方案, 循环放在同步的外面
思路转变:
*/
public class SellTicket extends Thread {
private static int tickets = 100;
@Override
public void run() {
// while (tickets > 0) { // 有票就卖
while (true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (SellTicket.class) {
// 没票就不卖了
if (tickets <= 0) {
break;
}
System.out.println(getName() + "卖了第" + (101 - tickets) + "张票.");
tickets--;
}
}
}
}
- Runnable方式
public class SellTicket implements Runnable {
// 由于这里使用的是实现接口方式, 所以如果测试类中只创建一个SellTicket对象的话
// 共享数据, 可以不加static
// 如果使用同步代码块的话, 锁对象也可以使用this
int tickets = 100;
@Override
public void run() {
// 无限循环
while (true) {
// 睡
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 同步代码块
synchronized (this) {
// 出口
if (tickets <= 0) {
break;
}
System.out.println(Thread.currentThread().getName() + "卖了第" + (101 - tickets) + "张票.");
tickets--;
}
}
}
}
volatile关键字
JMM: Java Memory Modle (Java内存模型)
-
可见性
-指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。显然,对于串行程序来说,可见性问题 是不存在。因为你在任何一个操作步骤中修改某个变量,那么在后续的步骤中,读取这个变量的值,一定是修改后的新值。但是这个问题在并行程序中就不见得了。如果一个线程修改了某一个全局变量,那么其他线程未必可以马上知道这个改动。 -
原子性
-所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。-比如:从张三的账户给李四的账户转1000元,这个动作将包含两个基本的操作:从张三的账户扣除1000元,给李四的账户增加1000元。这两个操作必须符合原子性的要求,要么都成功要么都失败。
多个操作是一个不可以分割的整体
-
有序性
-对于一个线程的执行代码而言,我们总是习惯地认为代码的执行时从先往后,依次执行的。这样的理解也不能说完全错误,因为就一个线程而言,确实会这样。但是在并发时,程序的执行可能就会出现乱序。给人直观的感觉就是:写在前面的代码,会在后面执行。有序性问题的原因是因为程序在执行时,可能会进行指令重排,重排后的指令与原指令的顺序未必一致 -
指令重排:可以保证串行语义一致,但是没有义务保证多线程间的语义一致,对于提高CPU处理性能是十分重要的
哪些指令不能重排:Happen-Before 规则
-
程序顺序原则:一个线程内保证语义的串行性
-
volatile规则:volatile 变量的写,先发生于读,这保证了volatile变量的可见性
-
锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
-
传递性:A先于B,B先于C,那么A必然先于C
-
线程的start()方法先于它的每一个动作
-
线程的所有操作先于线程的终结(Thread.join())
-
线程的中断(interrupt())先于被中断线程的代码
-
对象的构造函数执行,结束先于finalize() 方法
volatile修饰的变量
- 具有可见性
- 具有有序性, 禁止指令重排序
- 不能保证原子性
原子类
-
AtomicInteger
- 成员方法:
- 成员方法:
-
保证原子性的原因:
-
CAS:: Compare And Swap(比较再交换); 是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。CAS可以将read-modify-write
-
自旋锁:自旋锁的定义:当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取。这种采用循环加锁 -> 等待的机制被称为自旋锁(spinlock)。
-
底层代码解析
-
-
Synchronized是从悲观的角度出发:
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁
(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。因此Synchronized我们也将其称之为悲观锁。jdk中的ReentrantLock也是一种悲观锁。
CAS是从乐观的角度出发:
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。
CAS这种机制我们也可以将其称之为乐观锁。
volatile工作流程
volatile和synchronized区别
并发包
ConcurrentHashMap
- HashMap不具备线程安全,所以有了HashtaableMap,但由于HashtableMap运行速度慢所以就有ConcurrentHashMap
- CAS+局部同步锁