Java多线程

2019-09-14  本文已影响0人  DeeJay_Y

为什么需要多线程

开启一个新的线程

多线程引起的问题

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

举个例子,现有一个静态变量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的情况

适合多线程的场景

线程安全

线程不安全的表现

先来看一个例子:

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个线程拿着自己的锁,但是又在等对方释放自己需要的锁,形成死锁

排查死锁问题:

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

实现线程安全的手段

  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包

Object类里的线程方法

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:

线程的状态

线程的状态可以分为下列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

上一篇 下一篇

猜你喜欢

热点阅读