了解:线程有用户线程(非守护线程)和守护线程(后台线程)
- 问题引入:有一个COUNT = 0 变量,同时启动20个 线程,每个线程执行1000次,每次循环COUNT++,等这20个子线程执行完毕后,再main线程打印COUNT(预期2000),但是打印结果却总是小于预期值,这就涉及到线程安全问题。
public class UnsafeThread {
private static int COUNT = 0;
public static void main(String[] args) throws InterruptedException {
//有一个COUNT = 0 变量,同时启动20个 线程,每个线程执行1000次,每次循环COUNT++
//等这20个子线程执行完毕后,再main线程打印COUNT(预期2000)
Thread[] threads = new Thread[20];
for (int i = 0; i < 20;i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 1000;j++) {
COUNT++;
}
}
});
}
for (Thread t : threads) {
t.start();
}
//让main线程阻塞等待所有的20个线程执行完毕后再打印COUNT
for (Thread t : threads) {
t.join();
}
System.out.println(COUNT);
//执行结果每次都是小于20000
}
}
线程安全概念:如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说明这个程序线程安全。
线程不安全的原因:
1. 原子性(不能切分的最小单位):
多行指令(java某些代码,看着是一行,其实会分解为多条指令执行),如果代码行前后有依赖关系,不能插入其他影响我执行结果的指令(存在线程共享变量),如果能够插入就没有原子性,反之则有。
例1:methodA(new 对象()).menthidB() ---->methodA的返回对象调用methodB–》看着是一行,其实多行
例2:特殊的一行代码:(1)n++,n–,++n,–n,分解为三条指令 (2) new 对象:分解为三条指令
2. 可见性:系统调度cpu执行线程内某个方法,产生cpu视角的主存,即工作内存
主存:线程共享
工作内存:线程私有内存 + cpu高速缓存/寄存器
对主存中共享数据的操作,存在主存到工作内存 ==》从主存读取(拷贝)、工作内存修改、写回主存3. 有序性
例如:1.去前台取快递2.去教室写作业3.去前台拿U盘
如果是在单线程的情况下,JVM,CPU指令可能会对其进行优化,产生重排序,比如按照1 -> 3 -> 2顺序执行也可以,还提高了效率,这叫做指令重排序。
但是若在多线程情况下就会出现问题,例如可能快递是在写作业的十分重内被另一个线程放过来,此时如果指令重排序,代码就是错误的。
线程不安全问题的解决:加锁(关键字:synchronized )
1.作用:对一段进行加锁操作,让某段代码满足三个特性(即原子性、可见性、有序性)
2.原理:多个线程间同步互斥(一段代码在任意一个时间点只有一个线程执行:加锁、释放锁加锁–>基于对象来进行加锁/释放锁,而不是直接锁代码)
3.语法:
(1)同步代码块:synchronized(某个对象){…}
(2)同步方法:实例同步方法(this加锁)、静态同步方法(当前类对象加锁)
代码改进如下:在加锁之后就可以打印出预期值
同步代码块:
静态同步方法:
private static int COUNT = 0;
//synchronized加在static之前之后都可以
// public synchronized static void increment() {
// COUNT++;
// }
//对当前类对象进行加锁,线程间是同步互斥的
public static synchronized void increment(){
COUNT++;
}
—>调用
@Override
public void run() {
for (int j = 0; j < 1000; j++) {
increment();
}
}
实例同步方法:
//对this对象加锁
public static synchronized void increment(){
COUNT++;
}
—>调用
SynchronizedTest st = new SynchronizedTest();
Thread[] threads = new Thread[20];
for (int i = 0; i < 20; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 1000; j++) {
// synchronized (cl) { //加锁 同步代码块
// COUNT++;
// } //释放锁
// increment();//静态同步方法调用
//1.
st.increment(); //实例方法同步调用
//2.
synchronized (st) {
COUNT++;
}
}
}
});
}
注1:只有对同一个对象加锁,才会让线程产生同步互斥的作用
如下代码是对不同的对象加锁,没有同步互斥的作用
//对不同的对象加锁,没有同步互斥的作用,程序并发并行执行
public static void increment() {
synchronized (new SynchronizedTest()) {
COUNT++;
}
}
注2:synchronized具有可重入性(同一个线程可以对一个对象锁多次申请,基于计数器实现,申请获取+1,释放-1)
public static synchronized void decrement(){
COUNT--;
}
public static synchronized void increment(){
COUNT++;
decrement();
}
//调用increment()
//运行结果为0
注3:synchronized多个线程同步互斥
(1)一个时间只有一个线程执行(同步互斥)
(2)竞争失败的线程,不停的在阻塞态与运行态之间切换(用户态和内核态切换)
(3)同步线程数量越多其性能越低
代码:
public class SynchronizedTest {
private static int COUNT = 0;
//synchronized加在static之前之后都可以
// public synchronized static void increment() {
// COUNT++;
// }
// public static synchronized void increment(){
// COUNT++;
// }
public static void increment1() {
synchronized (new SynchronizedTest()) {
COUNT++;
}
}
//对this对象加锁
public synchronized void increment(){
COUNT++;
}
public static void main(String[] args) throws InterruptedException {
Class cl = SynchronizedTest.class;
SynchronizedTest st = new SynchronizedTest();
//有一个COUNT = 0 变量,同时启动20个 线程,每个线程执行1000次,每次循环COUNT++
//等这20个子线程执行完毕后,再main线程打印COUNT(预期2000)
Thread[] threads = new Thread[20];
for (int i = 0; i < 20; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 1000; j++) {
// synchronized (cl) { //加锁
// COUNT++;
// }
// increment();//静态同步方法调用
st.increment(); //实例方法同步调用
}
}
});
}
for (int i = 0; i < 1; i++) {
threads[19 + i] = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 1000; j++) {
st.increment();
}
}
});
}
for (Thread t : threads) {
if (t != null)
t.start();
}
for (Thread t : threads) {
if (t != null)
t.join();
}
System.out.println(COUNT);
//输出20000
}
}
******************************************************************************************************************
模拟实现:一个教室,座位有50个,同时有三个老师安排同学的座位, 每个老师安排100个同学,模拟使用多线程实现,座位编号为1-50 /0-49,三个线程同时启动来安排同学,同学可以循环操作来安排,一直到座位排满
public class SynchronizedTest2 {
private static int STUDENT = 50;
public static void main(String[] args) {
Task task = new Task();
for (int i = 0; i < 3;i++) {
new Thread(task).start();
}
}
private static class Task implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100;i++) {
synchronized (this) {
if (COUNT > 0) {
COUNT--;
System.out.println(Thread.currentThread().getName() + ":" + COUNT);
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main1(String[] args) throws InterruptedException {
Class cl = SynchronizedTest2.class;
Thread[] thacher = new Thread[3];
for (int i = 0; i < 3;i++) {
thacher[i] = new Thread(new Runnable() {
@Override
public void run() {
while (STUDENT > 0) {
synchronized (cl){ //加锁
if (STUDENT > 0) {
System.out.println(Thread.currentThread().getName() + ",还有" + STUDENT--);
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
}
for (Thread t : thacher) {
t.start();
}
}
}
若让老师安排的学生的人数可以控制应该如何修改?
代码如下:
public class SynchronizedTest3 {
private static int COUNT = 50;
public static void main(String[] args) {
new Thread(new Task(10)).start();
new Thread(new Task(20)).start();
new Thread(new Task(20)).start();
}
private static class Task implements Runnable{
private int number; //控制一个老师可以安排的数量
public Task(int number) {
this.number = number;
}
@Override
public void run() {
for (int i = 0; i < 100;i++) { //并发并行执行
synchronized (Task.class) {
if (COUNT > 0 && number > 0) { //一定要再次判断
COUNT--;
number--;
System.out.printf("%s:count=%s,num=%s\n" ,Thread.currentThread().getName(),COUNT,number);
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
volatile关键字:修饰共享变量
(1)保证可见性
(2)禁止指令重排序,建立内存屏障
(3)不能保证原子性
常见的使用场景:不依赖共享变量来赋值的操作,一般是进行读写分离操作,提高性能
(1)写操作不依赖共享变量,赋值是一个常量
(2)作用在读,写依赖于其他线程安全手段(加锁)
《依赖共享变量赋值不是原子性操作》
private static volatile boolean STOP = false;