Java多线程

线程的创建

实现 Runnable 接口

  1. 重写 Runnable 接口的 run 方法
  2. Thread(Runnable target) 构造函数
  3. 通过 Thread 代理对象调用 start 方法来启动线程
package website.yuchen;

class RunnableDemo implements Runnable
{
    @Override
    public void run() {
       // do something
        System.out.println("Hello");
    }
}

public class App
{
    public static void main( String[] args)
    {
        Runnable o = new RunnableDemo();
        new Thread(o).start();
    }
}

Lambda 表达式简化

  因为Runnable接口只有一个方法,因此可以直接用lambda表达式简化匿名类。

package website.yuchen;

public class App
{
    public static void main( String[] args)
    {
        /*
        new Thread(new Runnable() {
            @Override
            public void run() {
                //do something
            }
        }).start();
        */

        new Thread(()-> {
           //do something
        }).start();
    }
}

继承 Thread 类

  继承 Thread 类,重写 run 方法,通过 start 方法调用执行。

  1. 继承重写 Thread 类中的 run 方法
  2. 通过 Thread 对象调用 start 方法
package website.yuchen;

class ThreadDemo extends Thread
{
    @Override
    public void run() {
       // do something
    }
}

public class App
{
    public static void main( String[] args)
    {
        ThreadDemo t = new ThreadDemo();
        t.start();
    }
}

注:其中通过接口Runnable创建方式是更被推崇的



线程的五种状态

  • 运行态可以可以通过IO等方式进入阻塞态,而就绪态不能直接进入阻塞态
  • 阻塞结束回到就绪态等待处理机调度,而不能直接进入运行态

其中同步阻塞表示线程正处于锁池状态中


常见线程阻塞方法

  • synchronized:同步标识符,见下文线程同步
  • object.wait():当前运行线程进入等待阻塞,并让出临界资源
  • Thread.sleep(millis):阻塞当前线程一段时间
  • thread.join():让线程thread插队执行完,且当前线程阻塞(即令线程t与当前线程合并)

  join 方法示例:开一个线程t和主线程main并发执行,分别从0数到99,主线程数到50时,让线程t插队执行完:

package website.yuchen;

public class App
{
    public static void main(String[] args)
    {
        Thread t = new Thread(()-> {
            for( int i = 0; i < 100; i++)
                System.out.println("Thread t " + i);
        });  t.start();

        for( int i = 0; i < 100; i++) {
            try { if( i == 50) t.join(); }
              catch (InterruptedException e) {}

            System.out.println("Main Thread " + i);
        }
    }
}

sleep 和 wait 的区别

wait的一般写法搭配:

// 线程A
synchronized (object) {
    try { object.wait(); }
      catch(InterruptedException e) 
      { e.printStackTrace(); }
}

// 线程B
object.notify();    // 结束线程A的等待

  为什么wait方法在object类中,sleep方法在Thread类中?

object.wait()代表正在使用临界资源 object 的线程进入等待阻塞队列中,object的锁被释放,那么其他就绪的线程就可以访问这个 object 了。

  此外,其他线程并不知道具体哪个线程正在使用 object,只知道 object 有没有上锁 (由object的管程控制)。所以如果用Thread.wait()来表示的话,只是代表当前线程阻塞,也无从去释放 object 的锁。

  这也就是为什么 wait 方法只能放在 synchronized 代码块内。当 object.notify() 唤醒这个等待阻塞的线程时,此线程就进入锁池,在锁可用时进入就绪态。


  一言以蔽之,最核心的是:同样是阻塞,wait会释放**资源锁**,但sleep不会。下述代码表现了这个特点:
package website.yuchen;

public class App
{
    public static void main(String[] args)
    {
        Object o = new Object();

        new Thread(()->
        {
            synchronized (o)
            {
                System.out.println("Thread1 is going to wait..");

                try { o.wait(); }
                  catch (InterruptedException e)
                  { e.printStackTrace(); }

                System.out.println("Thread1 get up!");
            }
        }).start();

        new Thread(()->
        {
            synchronized (o)
            {
                System.out.println("Thread2 is going to sleep..");

                o.notify(); // 睡前通知Thread1起来
                try { Thread.sleep(1000); }
                  catch (InterruptedException e)
                  { e.printStackTrace(); }

                System.out.println("Thread2 get up!");
            }
        }).start();
    }
}

/* 输出结果
Thread1 is going to wait..
Thread2 is going to sleep..
Thread2 get up!
Thread1 get up!
*/


线程同步 —— synchronized关键字

  以下是一个线程不安全的代码:开很多个线程往LinkedList里加元素

package website.yuchen;

import java.util.*;

public class App
{
    public static void main(String[] args) throws InterruptedException
    {
        List<String> ls = new LinkedList<>();
        Set<Thread> threads = new HashSet<>();

        for(int i = 0; i < 1000; i++)
        {
            Thread t = new Thread(()-> {
                ls.add(Thread.currentThread().getName());
            });  t.start();
            threads.add(t);
        }

        for( Thread t: threads) t.join();   // 主线程等待子线程完成
        System.out.println(ls.size());  // 结果可能少于1000(线程不安全)
    }
}

代码块

  解决以上线程不安全问题可以给容器加同步锁 ,或者使用线程安全的容器CopyOnWriteArrayList

package website.yuchen;

import java.util.*;

public class App
{
    public static void main(String[] args) throws InterruptedException
    {
        List<String> ls = new LinkedList<>();
        // List<String> ls = new CopyOnWriteArrayList<>();  // 线程安全容器
        Set<Thread> threads = new HashSet<>();

        for(int i = 0; i < 1000; i++)
        {
            Thread t = new Thread(()-> {
                synchronized (ls) { // 代码块同步锁
                    ls.add(Thread.currentThread().getName());
                }
            });  t.start();
            threads.add(t);
        }

        for( Thread t: threads) t.join();
        System.out.println(ls.size());  // 输出 1000
    }
}

方法

  方法的同步就相当于代码块同步锁synchronized (this)的写法,方法同步锁写起来似乎更无脑一些。

package website.yuchen;

import java.util.*;

class MyList extends LinkedList {
    @Override
    public synchronized boolean add(Object o) { // 方法锁
        return super.add(o);
    }
}

public class App
{
    public static void main(String[] args) throws InterruptedException
    {
        Set<Thread> threads = new HashSet<>();
        List<String> ls = new MyList();

        for(int i = 0; i < 1000; i++)
        {
            Thread t = new Thread(()-> {
                ls.add(Thread.currentThread().getName());
            });

            t.start();
            threads.add(t);
        }

        for( Thread t: threads) t.join();
        System.out.println(ls.size());  // 输出 1000
    }
}

死锁

  多个并发进程因争夺系统资源而产生相互等待的现象。

四个必要条件:

  1. 资源只能互斥访问
  2. 进程自身占有资源又保持对其他资源的申请
  3. 进程资源不可被剥夺
  4. 多个进程之间形成循环等待的关系

比如这段代码多运行几次就很容易发生死锁:

package website.yuchen;

public class App
{
    public static void main(String[] args)
    {
        Object o1 = new Object();
        Object o2 = new Object();

        new Thread(()->
        {
            synchronized (o1)
            {
                System.out.println("Thread1 keeps o1..");
                System.out.println("Thread1 applies for o2..");

                synchronized (o2) {
                    System.out.println("Thread1 get o2!");
                }
            }
        }).start();

        new Thread(()->
        {
            synchronized (o2)
            {
                System.out.println("Thread2 keeps o2..");
                System.out.println("Thread2 applies for o1..");

                synchronized (o1) {
                    System.out.println("Thread1 get o1!");
                }
            }
        }).start();
    }
}

生产消费者模型

  生产消费者模型是多线程并发中的一个经典问题。比如在食堂排队取餐,后厨厨师进行生产,与此同时学生进行购买消费。这就需要做到多线程下的资源协调。

class Resource {
    int id;
    Resource(int id) { this.id = id; }
}

class Producer {
    Queue<Resource> buffer;
    Producer(Queue buffer) { this.buffer = buffer; }

    static int ver = 0;
    public synchronized void produce() {    // 多个生产者之间的同步锁
        synchronized (buffer) {     // 生产者与消费者之间打印的信息协调
            buffer.offer(new Resource(++ver));
            System.out.println("Producer produce resource " + ver);
        }
    }
}

class Consumer {
    Queue<Resource> buffer;
    Consumer(Queue buffer) { this.buffer = buffer; }

    public synchronized void consume() {
        synchronized (buffer) {
            Resource t = buffer.poll();
            System.out.println("Consumer consume resource " + t.id);
        }
    }
}

生产者和消费者之间交互通过缓冲区来解耦

  借助wait和notify方法实现,这两个方法需要处于synchronized范围内

  • object.wait():当前运行线程进入等待阻塞,并让出临界资源
  • object.notifyAll():唤醒全部等待池中的线程
class buffererQueue extends LinkedList
{
    static int MAXN = 20;

    @Override
    public synchronized boolean offer(Object o) // 这里不加synchronized也可以,因为调用前已对buffer上锁
    {
        while( this.size() >= MAXN) {   // 缓冲区满,生产者阻塞等待
            try { this.wait(); }
              catch (InterruptedException e)
              { e.printStackTrace(); }
        }   // 使用 while 比 if 更安全

        boolean res = super.offer(o);
        this.notifyAll();   // 生产者生产了资源,唤醒等待的消费者
        return res;
    }

    @Override
    public synchronized Object poll() 
    {
        while( this.size() <= 0) { // 缓冲区空,消费者阻塞等待
            try { this.wait(); }
              catch (InterruptedException e)
              { e.printStackTrace(); }
        }

        Object res = super.poll();
        this.notifyAll();   // 消费者消耗了资源,唤醒等待的生产者
        return res;
    }
}

  以下是多对多的生产消费者模型:生产者和消费者各100个,每个生产者生产1000份,消费者消耗1000份,协同运作:

public class App
{
    public static void main(String[] args)
    {
        Queue<Resource> buffer = new buffererQueue();
        Producer producer = new Producer(buffer);
        Consumer consumer = new Consumer(buffer);

        for( int k = 0; k < 100; k++) {
            new Thread(()-> {
                for( int i = 0; i < 1000; i++)
                    producer.produce();
            }).start();
        }

        for( int k = 0; k < 100; k++) {
            new Thread(()-> {
                for( int i = 0; i < 1000; i++)
                    consumer.consume();
            }).start();
        }
    }
}


其他

线程优先级

  • thread.setPriority(newPriority)
  • thread.getPriority()

  优先级 newPriority 介于1~10之间,默认是5

  优先级高的线程并不代表能独占cpu,而是宏观上来看被调度的几率更大。

  由于Java语言层面的线程的实际上还会被映射到对应OS的原生线程上,因此Jvm主流的线程的调度方式取决于OS所支持的线程模型。

  简单拓展一下:比如历史上Linux系统曾采用 1:1 的线程模型,即一个轻量级进程(即用户线程)对应一个内核线程,由线程调度器来分配cpu资源,而此时Jvm对于线程的操作是通过映射到用户线程实现的,也是 1:1 的关系。


守护线程

  Java守护线程为用户线程而服务,但用户线程全部结束时,守护线程即使还有语句没能执行也会随之结束。

  常用于程序日志打印等服务

  • thread.setDaemon(true)
package website.yuchen;

public class App
{
    public static void main(String[] args)
    {
        Thread guard = new Thread(()-> {
            while(true) System.out.println("Daemon");
        });
        guard.setDaemon(true);
        guard.start();

        new Thread(()-> {
            for( int i = 0; i < 1000; i++)
                System.out.println("Thread");
        }).start();
    }
}

Java 多线程内存模型

  在Java中每个线程都有一个工作内存,工作内存中保存了被该线程使用到的变量的主内存副本拷贝。
  这就可能会导致不同工作内存中的数据不同的问题。

保证数据的同步性 —— volatile 关键字

volatile是一种轻量级的锁,它能保证锁的可见性,但不能保证锁的原子性(synchronized既能保证原子性又能保证可见性)。

  也就是用volatile修饰的变量每次使用读取的都会是直接与主存同步的最新的值。

  以下是一个volatile的经典应用,如果去掉flag变量的volatile标识则很可能会陷入死循环:

package website.yuchen;

public class App
{
    static volatile boolean flag = true;

    public static void main(String[] args) throws InterruptedException
    {
        new Thread(()-> {
            while(flag);
        }).start();

        Thread.sleep(500);
        flag = false;
    }
}

  发生死循环的原因是因为Jvm对于这种反复高密度的简单指令优化更加激进(指令重排),从而无暇去同步工作内存中所对应的值。

  如果循环中出现比较复杂的算法或慢速指令比如计算随机数、打印输出等就不会出现死循环。volatile还有一种经典的应用就是配合单例模式的实现,是为了防止创建对象时指令重排,这里就不展开了。

要点:

  • 与主存直接同步
  • 禁止指令重排
  • 不保证语句原子性
  • 修饰变量

可重入锁

  可重入就是一个线程可以多次获取同一个锁

比如因为可重入性,以下代码并不会产生死锁:

synchronized (this) {
    synchronized (this) {
        // ...
    }
}

ReentrantLock 用法示意

ReentrantLock是一种可重入的锁,包含synchronized关键字的功能且提供许多其他功能。

class Foo {
    private final ReentrantLock lock = new ReentrantLock();
    // ...

    public void bar() {
        lock.lock();  // block until condition holds
        try {
            // ... method body
        } finally {
            lock.unlock()
        }
    }
 }

可重入锁的实现

  对锁记录正在使用的线程以及该线程重入次数的计数

package website.yuchen;

/* 不可重入锁
class Lock
{
    private boolean isLocked = false;

    public synchronized void lock() throws InterruptedException
    {
        while(isLocked) wait();
        isLocked = true;
    }

    public synchronized void unlock() {
        isLocked = false;
        notify();
    }
}*/

class Lock
{
    private boolean isLocked = false;
    private int cnt = 0;
    private Thread thread = null;

    public synchronized void lock() throws InterruptedException
    {
        Thread tcur = Thread.currentThread();
        while(isLocked && thread != tcur) wait();

        cnt ++;  thread = tcur;
        isLocked = true;
    }

    public synchronized void unlock() {
        if( --cnt > 0) return ;
        isLocked = false;  thread = null;
        notify();
    }
}


public class App
{
    public static void main(String[] args) throws InterruptedException
    {
        Lock lock = new Lock();

        lock.lock();
        {
            System.out.println("Main Thread get lock.");

            lock.lock();
                System.out.println("Main Thread get lock again..");
            lock.unlock();
        }
        lock.unlock();
    }
}


  以上为个人学习总结,如有问题欢迎提出。

-------------本文结束-------------
0%