第一章 多线程入门之概念
https://blog.csdn.net/qq_41714995/article/details/124749473?spm=1001.2014.3001.5501
第二章 多线程入门之基础
https://blog.csdn.net/qq_41714995/article/details/124758179?spm=1001.2014.3001.5501
第二章 多线程入门之高级
文章目录
- 第二章 多线程入门之高级
- 前言
- 一、引入共享变量
- 1.1 小故事
- 1.2 临界区和竞态条件
- 1.3 学好synchronized
- 1.3.1 sychronized的小故事
- 1.3.2 synchronized 的使用方法
- 代码块
- 方法修饰符
- 1.3.3 变量的线程安全分析
- 成员变量和静态变量是线程安全的吗
- 局部变量是线程安全的吗
- 1.3.4 Monitor 概念
- Java对象头
- Monitor原理图
- 1.3.5 synchronized的高级
- 小故事
- 1.4 常见线程安全类
- 1.4.1 常见的线程安全类
- 1.4.2 不可变类的设计
- String
- Integer
- 实例分析
- 1.4.3 卖票问题
- 1.4.4 转账问题
- 总结
前言
本文主要记录了join方法
一、引入共享变量 1.1 小故事
之前笔者一直对于多线程中共享变量被脏写没有很清晰的认识,但是随着后来学习的不断深入加之某天看到b站上满一航老师对于共享变量被脏写的例子,真的醍醐灌顶现在将其加工之后分享出来。
假设隔壁老王是一个黑心老板,他只有一台电脑让员工工作。小红和小明共同使用这台电脑操作同一个数据,小明负责+1小红负责-1。但是问题来了:小明可能要睡觉啊,上厕所啊,吃饭啊,不能全天工作,小红也是如此。那么老王就觉得这可不行太亏了。干脆这样给小明分配一段时间,给小红分配一段时间那么他们可以趁着没给分配电脑的时间段睡觉、上厕所、吃饭…
正常情况下,小明使用完小红使用,最后的res=0没有任何问题。如下图所示:
可是有一天小明将res=1之后病倒了,老王看小明病倒了实在不能工作了就告诉小明那你先记着这个res的结果,等你病好了再把res放进去。小红抓紧来干活儿,于是小红继续-1并且将结果放回电脑中,小明病好了之后就继续把之前的res=1放回电脑中。那么大家发现问题了没有,本来两个人操作之后结果应该是0,但是现在的结果却是1。这是不是就出现了共享变量脏写的问题呢?如下图所示:
1.2 临界区和竞态条件我们用一段代码举例吧:
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
count++;
}
}, "线程一");
Thread thread2 = new Thread(()->{
for (int i = 0; i < 10000; i++) {
count--;
}
}, "线程二");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(count);
}
//运行结果:1570
这段如果对于join()不太清楚可以看我之前的文章
我们使用时序图研究一下上面的问题出现的原因。
对于i++而言会产生下面的Jvm字节码指令
getstatic i //获取静态变量i的值 iconst_1 //准备常量1 iadd //自增 putstatic //将自增的结果存入静态变量i
对于i–而言会产生下面的Jvm字节码指令
getstatic i //获取静态变量i的值 iconst_1 //准备常量1 isub //自- putstatic //将自增的结果存入静态变量i
作为小白的你可能很难明白上面的字节码指令,我刚刚开始的时候也是非常蒙的,如果你学了Jvm可能会好很多。你可以这样想java作为一门高级语言,计算机底层肯定不能理解的,必须转为更加直白的指令计算机才能理解,Jvm指令便是这个指令。线程执行程序的时候你以为是在执行java代码,但其实执行的是Jvm指令。
临界区:一段代码中存在着对共享变量的读和写,这段代码成为临界区,上面代码中的count++和count–就是临界区。
竞态条件:通过执行临界区的代码而发生了线程安全的问题就是发生了竞态条件
当发生了竞态条件可以使用阻塞式和非阻塞式两种解决方法,阻塞式即意味着加锁,非阻塞式即意味着原子变量(原子变量小白不用着急懂,慢慢来 ^^) 。锁主要有两种,一种是sychronized,一种是lock。我们今天来学习synchronized。
1.3.1 sychronized的小故事我们接着上面的例子,老王发现上面那样做可不行,小明没有做完工作小红就继续做公司账目都乱套了。于是他想了一个好办法,小明使用这唯一一台电脑的时候需要加锁,小明必须把工作做完才能将锁打开给小红使用。(这期间老王也会将电脑使用权交给小红,只是电脑被小明锁住了小红不能工作)
sychronized(对象) {
代码...
}
方法修饰符
public sychronized f1(){
代码...
}
相当于
sychronized(this) {
代码...
}
public static sychronized f2() {
代码...
}
相当于
sychronized(当前类.class) {//当前类对象只有一份
代码...
}
如果想让临界区不发生竞态条件必须要保证sychronized加的锁时是一个对象
1.3.3 变量的线程安全分析 成员变量和静态变量是线程安全的吗如果这部分理解的不清楚的话,我们可以搜一下线程八锁
TODO 线程八锁链接http://t.csdn.cn/1gGrg
- 如果变量没有共享,那么线程安全
- 如果变量共享但是只是读操作,那么线程安全
- 如果变量共享有写操作,那么线程不安全
- 局部变量是线程安全的
- 但是如果有逃逸问题,那么就是线程不安全的
1.3.4 Monitor 概念 Java对象头线程逃逸问题TODO
普通对象
|-----------------------------------------------------------| | Object Header (64 bits) | |---------------------------------|-------------------------| | Mark Word (32 bits) | Klass Word (32 bits) | |---------------------------------|-------------------------|
数组对象
|------------------------------------------------------------------------------| | Object Header (96 bits) | | | |---------------------------------|--------------------|-----------------------| | Mark Word (32 bits) | Klass Word(32 bits)| array length(32bit) | |---------------------------------|--------------------|-----------------------|
|-----------------------------------------------------------------------------------------------------------------| | Object Header(64bits) | |-----------------------------------------------------------------------------------------------------------------| | Mark Word(32bits) | Klass Word(32bits) | State | |-----------------------------------------------------------------------------------------------------------------| | hashcode:25 | age:4 | biased_lock:0 | 01 | OOP to metadata object | Nomal | |-----------------------------------------------------------------------------------------------------------------| | thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | OOP to metadata object | Biased | |-----------------------------------------------------------------------------------------------------------------| | ptr_to_lock_record:30 | 00 | OOP to metadata object | Lightweight Locked | |-----------------------------------------------------------------------------------------------------------------| | ptr_to_heavyweight_monitor:30 | 10 | OOP to metadata object | Heavyweight Locked | |-----------------------------------------------------------------------------------------------------------------| | | 11 | OOP to metadata object | Marked for GC | |-----------------------------------------------------------------------------------------------------------------|Monitor原理图
当我们写出synchronized(obj){}这样的代码的时候,我们便是将obj和monitor对象进行关联,关联的方法是通过obj对象头中的markword。线程一执行临界区代码的时候便会通过obj对象找到monitor对象看一下 owner并没有值,那么owner便会指向线程一,如果此时线程二也过来执行临界区代码,此时发现owner是有值,那么entryList便会指向线程二,直到线程一执行完临界区的代码,线程二才会被唤醒。(如果线程二、线程三都在entryList中,并不是公平的意味着线程二虽然在线程三的前面,但是线程二和线程三也是同时竞争的)
- String
- Integer
- StringBuffer
- Random
- Vector
- HashTable
- java.util.concurrent
注意:调用里面的每一个方法可以保证原子性,但是方法的组合不是原子性的,下面这端代码就不能保证线程安全性。
Hashtable table = new Hashtable();
if(table.get("key") == null) {
table.put("key",value);
}
1.4.2 不可变类的设计
String Integer 实例分析TODO 其实我也不理解 学完添加
//是否是线程安全的?--->不是 Mapmap = new HashMap<>(); //是否是线程安全的?--->是 String s1 = "..."; //是否是线程安全的?--->是 final String s2 = "..."; //是否是线程安全的?--->不是 Date d1 = new Date(); //是否是线程安全的?--->不是,因为虽然d2不能被改变,但是Date()里面的属性可以被改变 final Date d2 = new Date();
最后一个例子好好理解一下
public class Myservlet extends HttpServlet {
private UserService userservice = new UserService();
public void doGet(HttpServletRequest request,HttpServletResponse response) {
userservice.update();
}
}
class UserService{
//是否是线程安全的?--->不是
private int count = 0;
public void update() {
count++;
}
}
@Aspect
@Component
public class MyAspect {
//是否是线程安全的?--->不是
private long start =0L;
@Before("execution(* *(..))")
public void before() {
start = System.nanoTime();
}
@After("execution(* *(..))")
public void after() {
Long end = System.nanoTime();
System.out.println("costTime" + (end-start));
}
}
public class Myservlet extends HttpServlet {
//是否安全?--->是
private UserService userservice = new UserService();
public void doGet(HttpServletRequest request,HttpServletResponse response) {
userservice.update();
}
}
class UserService{
//是否安全?--->是
private UserDao userDao = new UserDao();
public void update(){
userDao.update();
}
}
class UserDao{
public void update(){
String sql = "update user set ... where ...";
//局部变量线程是否安全?--->是
try(Connection connection = DriverManager.getConnection("","","")) {
}catch (Exception e) {
}
}
}
public class Myservlet extends HttpServlet {
//是否安全?--->不是
private UserService userservice = new UserService();
public void doGet(HttpServletRequest request,HttpServletResponse response) {
userservice.update();
}
}
class UserService{
//是否安全?--->不是
private UserDao userDao = new UserDao();
public void update(){
userDao.update();
}
}
class UserDao{
private Connection conn = null;
public void update() throws Exception{
String sql = "update user set ... where ...";
//局部变量线程是否安全?--->不是
conn = DriverManager.getConnection("","","");
conn.close()
}
}
总结:一个类中包含的成员变量:如果是基本类型,那么我们就看本类中没有直接修改这个成员变量的方法,如果有的话那么就是线程不安全的;如果是引用类型,那么我们就看这个对象里面还有没有成员变量,如果有的话我们在按照上面的分析。
下面还有一种情况没有成员变量,但是还是可能出现线程不安全的问题
public abstract class Test2 {
public void bar(){
SimpleDateFormat sdf = new SimpleDateFormat();
foo(sdf);
}
abstract void foo(SimpleDateFormat sdf);
public static void main(String[] args) {
new Test2().bar();
}
}
class Test2Son extends Test2{
String date = "";
@Override
void foo(SimpleDateFormat sdf) {
new Thread(()->{
try {
sdf.parse(date);
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
1.4.3 卖票问题 1.4.4 转账问题注意开闭原则:比如f1()调用f2(),那么并且传递给f2()一个变量sdf,如果f1()中对于sdf进行修改,同时f2()中新开了一个线程也对于该变量进行修改,那么可能会引起线程不安全的问题。
总结
本文主要概述了多线程中共享变量的问题



