Java多线程

为什么需要多线程

  • 参见之前的文章《CPU: 这个时间太慢了》
  • 现代的CPU都是多核的
    受散热量制约,目前的CPU大多都是3Ghz,再高的话就需要专业设备来进行降温,所以当代提升性能的方式是堆核,造成了目前的CPU都是多核的情况
  • Java的执行模型是同步/阻塞的
  • 默认情况下只有一个线程:
    • 处理问题非常自然
    • 但是也有严重的性能问题

开启一个新的线程

  • Thread
    • Java中唯一的代表线程的东西
    • 只有start()方法才能开始并发执行
    • 每多开一个线程,就多一个执行流
    • 方法栈(局部变量)是线程私有的
    • 静态变量/类变量是所有线程共享的

多线程引起的问题

多线程难的地方在于:要看着同一份代码,想象着不同的人在以疯狂的乱序执行它

举个例子,现有一个静态变量i,则其可以被多个线程共享,在每一个线程中将其加1,循环1000次,最终得到的结果不一定为1000(每次运行的结果也不一样)

public static int i = 0;

public static void main(String[] args) {
  for(int a = 0; a < 1000; a++){
    new Thread(() -> modifySharedVar).start();
  }
}

public static void modifySharedVar() {
  try{
    Thread.sleep(1);
  }
  catch(Exception e) {
    e.printStackTrace();
  }
  i++;
  System.out.println(i);
}

造成上述问题的愿意在于:i++;这个操作不是一个原子操作。其分为三步:

  1. 取i的值
  2. 将取的值+1
  3. 将值写回i

这就有可能造成一个问题,假设此时i为0,线程1取到0然后加1,走完第2步时阻塞了未来得及将1写回i,这是线程2取i的值还是0,将其加1写回i,然后线程1恢复也将1写回i。

上述原因就造成了最终结果不为1000的情况

适合多线程的场景

  • 对于IO密集型应用极其有用
    • 网络IO(通常包括数据库)
    • 文件IO
  • 对于CPU密集型应用稍有折扣
  • 性能提升的上限在哪?
    • CPU:占用100%

线程安全

  • 享用了多线程的便利,就要承受多线程带来的问题
    • 原子性
    • 共享变量
    • 默认的实现几乎都不是线程安全的

线程不安全的表现

  • 数据错误
    • i++
    • if-then-do

先来看一个例子:

import java.util.HashMap;
import java.util.Map;
import java.util.Random;
  
public class Main {
    private static Map<Integer, Integer> map = new HashMap<Integer, Integer>();// HashMap非线程安全  死锁问题
    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            new Thread(Main::putIfAbsent).start();
        }
    }


    private static void putIfAbsent() {
//        随机生成一个1-10之间的数字 如果不在Map中,就将其加入Map
        int r = new Random().nextInt(10);
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        if(!map.containsKey(r)) {
            map.put(r, r);
            System.out.println("put " + r);
        }
    }
}

上述例子中出现了一个的Map put多次相同key的情况

接下来手动实现一个死锁:

public class Main {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    static class Thread1 extends Thread{
        @Override
        public void run() {
            synchronized (lock1) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (lock2) {
                    System.out.println("never print");
                }
            }

        }
    }

    static class Thread2 extends Thread {
        @Override
        public void run() {
            synchronized (lock2) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (lock1) {
                    System.out.println("never print");
                }
            }
        }
    }

    public static void main(String[] args) {
        new Thread1().start();
        new Thread2().start();
    }
}

分析一下上述死锁形成的原因:

  1. Thread1和Thread2几乎是同时启动的,启动之后,T1先拿到lock1锁,然后休眠500ms,T2先拿到lock2锁,开始休眠100ms
  2. T2休眠时间较短,先醒过来,然后T2需要拿到lock1锁,但是此时lock1被T1拿着,T2被迫一直在等lock1锁释放
  3. T1后醒过来,此时T1想要拿到lock2锁,但是由于lock2锁在被T2拿着,所以T1也被迫等lock2释放
  4. 2个线程拿着自己的锁,但是又在等对方释放自己需要的锁,形成死锁

排查死锁问题:

  • ps aux | grep java (unix下)
  • 使用jps 列出当前所有java进程(通用)
    1. 终端输入jps列出当前进程,上述例子中出现死锁的地方为Main,拿到PID ,例如4756
    2. jstack 4756查看栈信息(一般信息较长,可以导出到文件中查看),可以查看到信息"Found one Java-level deadlock:",可以看到详细的信息

预防死锁发生的原则:所有的线程都按照相同的顺序拿到锁

实现线程安全的手段

  1. 使用不可变类

    • Integer/String/...等等
  2. synchronized

    • synchronized同步块
    • 同步块t同步了什么东西?
      1. synchronized(object) 将object当成一个锁
      2. static方法上使用synchronized,由于static是和类绑定,所以锁住的是当前的class对象,即把这个Class对象当成锁
      3. 如果不是static方法,那么方法上的synchronized会将this当成锁,即锁住的是这个实例对象
    • Collections.synchronized
      常见的Collection都不是线程安全的,Collections工具类提供了synchronizedXXX方法将对应的集合转为线程安全的。

现在使用synchronized来修复之前共享静态变量的问题:

public class Main {
    private static int i = 0;
    private static Object lock1 = new Object(); // 声明一个锁用来同步 保证同一时刻只有一个线程可以访问到i
    public static void main(String[] args) {
        for (int i = 0; i < 1000; i ++) {
            new Thread(() -> ModifySharedVar()).start();
        }
    }
    private static void ModifySharedVar() {
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        synchronized (lock1) { // 在这使用synchronized同步块来保证同时只有一个线程修改i
            i += 1;
        }

        System.out.println("i = " + i);
    }
}

synchronized还有多种变化形式,对于上述方法,可以直接写为:

private synchronized static void ModifySharedVar() { // 写到方法上
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        i += 1;
        System.out.println("i = " + i);
    }

达到的结果也是一样的。

对于实例上的方法,如果加了synchronized的,那么等价于:

    public synchronized void method1 () {
        doSomeThing();
    }
//    等价于:
    public void method2() {
        synchronized (this) {
            doSomeThing();
        }
    }
  1. 使用第三方的实现类
    JUC包
  • AtomicInteger
  • ConcurrentHashMap 任何使用HashMap有线程安全问题的地方,无脑使用ConcurrentHashMap替换即可
  • ReentrantLock 可重入锁 比synchronized要强大,可以手动unlock

Object类里的线程方法

  • 为什么Java中的所有对象都可以成为锁
    • Object.wait()/notify()/notifyAll()方法
    • 线程的状态与线程调度

wait()和notify()方法为线程协作提供了方法

wait()方法的注释:Causes the current thread to wait until it is awakened, typically by being <em>notified</em> or <em>interrupted</em>.
@throws IllegalMonitorStateException if the current thread is not * the owner of the object's monitor
可以看到,如果调用wait方法的时候,没有持有当前调用wait方法的对象时,是会抛出IllegalMonitorStateException异常的:

//    public static void main(String[] args) throws InterruptedException {
//        lock1.wait(); // 会抛出异常
//    }

    public static void main(String[] args) throws InterruptedException {
        synchronized (lock1) {
            lock1.wait();
        }
    }

monitor即为常规意义上的“拿到的锁”

值得注意的是,lock.wait()调用了之后,lock这个锁会被释放掉! (为之后notify做准备)

关于notify和notifyAll:

  • notify会随机挑选一个线程来进行唤醒,挑选方式由JVM自己决定
  • notifyAll则会唤醒所有的线程,这些被唤醒的线程则会开始重新竞争锁,但是只有拿到锁的那一个线程会继续执行逻辑,其余线程则又会陷入沉睡,且所有线程竞争时没有任何优先级
线程的状态

线程的状态可以分为下列6种:

  1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
  2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
    线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
  3. 阻塞(BLOCKED):表示线程阻塞于锁。
  4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
  5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
  6. 终止(TERMINATED):表示该线程已经执行完毕。

上述修改共享静态变量的例子,只有当前拿到锁的线程为runnable状态,同一时间其他的线程为blocked状态

而对于调用了monitor.wait()方法的线程,则会进入waiting状态。

相关文章: 线程的状态

多线程的经典问题: 生产者/消费者模型

使用三种方法来实现生产者/消费者模型:

  1. wait/notify/notifyAll

  2. Lock/Condition
    将lock变为一个ReentrantLock,将一个synchronized快变为一个try/catch,进入上锁,finally解锁

  3. BlockingQueue

线程池和Callable/Future

  • 什么是线程池
    • 线程是昂贵的(Java线程模型的缺陷)
      Java线程模型的缺陷是其自己是没有调度器的,每个线程调度都依赖于操作系统的线程调度,因此每个线程都要占用操作系统的资源,所以其非常的昂贵
    • 线程池是预先定义好的若干个线程
      不用每次都先创建新的线程
    • Java中的线程池
  • Callable/Future
    • 类比Runnable, Callable可以返回值,抛出异常
    • Future代表一个未来才会返回的结果
     ExcutorService threadPool;
     Future f1 = threadPool.submit(Callable..)
    
  • 多线程的WordCount
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容