03 多线程

多线程实现方法

使用 实现 Runnable 接口的方式, 实现了 Runnable 的类的实例由线程创建. 必须实现 run 方法.

用户线程: 默认情况下, 线程都是用户线程

守护线程: 用来守护用户线程的.

run 这种多线程的方法: (假装 Employee 实现了 runnable 方法

Employee emp = new Employee();

new Thread(emp).start();   // 交给 CPU 去调用, CPU 具体什么时候执行, 我们不知道. 等时间片轮转, 并不是直接执行run(), start 才是开启新的线程, start 会自己调用 run 方法.

package com.My.Thread;

public class My12306 implements Runnable {
    private int ticketNum = 99;    
    
    public static void main(String[] args) {
        
        new Thread(new My12306(), "one").start();
        new Thread(new My12306(), "two").start();
        new Thread(new My12306(), "three").start();
    }

    @Override
    public void run() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        while (ticketNum > 0) {
            System.out.println(Thread.currentThread().getName() + "-->" + ticketNum--);
        }
        
    }

}

龟兔赛跑

package com.My.Thread;

public class MyRace implements Runnable {
    // 这块必须使用 static 生命, 这样多个线程之间才使用的是同一个 winner
    private static String winner;
    public static void main(String[] args) {
        new Thread(new MyRace(), "rabbit").start();
        new Thread(new MyRace(), "tortoise").start();
    }

    @Override
    public void run() {
        for (int step=1; step<=100; step++) {
            System.out.println(Thread.currentThread().getName() + "==>" + step);
            if (gameOver(step)) {
                break;
            } else {
                if(Thread.currentThread().getName().equals("rabbit") && step % 10 == 0) {
                    try {
                        Thread.sleep(2);    // 兔子总睡觉
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    public boolean gameOver(int step) {
        if (null != winner) {
            return true;
        } else if (100 == step) {
            winner = Thread.currentThread().getName();
            System.out.println("WInner is " + winner);
            return true;
        }
        return false;
    }

}

 线程状态

进入就绪状态, 4种方法:

1) start

2) 阻塞事件解除 -> 回到就绪状态

3) yield 方法, 让出CPU, 那么本线程进入就绪状态, Thread.yield(), 本线程谦让出CPU之后立刻进入就绪状态,

  所以很有可能下一个时间片它又再次获得 CPU 来进入运行状态.

4) JVM 本身将CPU 从本线程切换到其他线程

进入阻塞状态, 4种方法:

1) sleep, 该线程继续占用对象资源, Thread.sleep(200)

2) wait, 本线程让出 CPU 和 对象资源

3) join, 插队,a 线程调用join, a.join(), a.join()这个语句被写到哪个b线程里, b线程被阻塞, 而且要b等到a线程本身执行完毕, b线程才能继续执行.

4) I/O 操作, read, write, 必须通过操作系统去调度

运行状态 : 当就绪状态的线程被 CPU 执行. 注意必须是从就绪状态来的. 阻塞状态不能直接执行.

死亡状态:

1) 线程自己运行完.

2) 当标记执行完的 flag 被外部触发, 线程执行完毕. 举例如下:

package com.My.Thread;

public class ControlTerminateThread implements Runnable {
    private boolean flag = true;
    private String name;

    public ControlTerminateThread(String name) {
        super();
        this.name = name;
    }
    @Override
    public void run() {
        int i = 0;
        while (flag) {
            System.out.println(this.name + "-->" + i++);
        }
    }
    public void terminate() {
        this.flag = false;
        System.out.println(this.name + "dead");
    }
    public static void main(String[] args) {
        ControlTerminateThread cl = new ControlTerminateThread("C罗");
        new Thread(cl).start();
        for (int i = 0; i < 100; i++) {
            if (i == 88) {
                cl.terminate();
            }
            System.out.println("main" + "-->" + i);
        }
    }
}

线程的优先级就是获得 CPU 的概率, 默认是 5, 范围: 1-10

getPriority(), setPriority(MIN_PRIORITY)

线程分类

1. 用户线程(默认的)

2.守护线程:为用户线程服务,JVM停止不用等待守护线程结束

Thread t = new Thread(god);  // god 是一个实例(当然类是实现了Runnable接口的)

t.setDaemon(true)  // 将这个线程设置成守护线程, 注意一定要在t.start()之前来设置

t.start();

 常用的方法

线程同步 / 线程安全 Synchronized

如果线程本身只是读一个对象, 没有改的操作, 那么不用保证线程安全.

并发: 多个线程同时操作同一个对象.

保证线程安全, 排队, 对资源(对象)加锁.  每个对象本身就有一个锁, 当线程想操作对象时,需要先获得这个锁.

 锁谁?

锁对象:

给你一个对象让你锁定. (同步块)

锁 this (如果你操作的是一个成员方法, 那 this 就是当前对象本身, 如果是一个静态方法, 那 this 就是指类模板本身), 锁的最小单位就是对象, 

  所以说, 如果没有特殊给出要锁谁, 只是单纯的在方法前加 synchronized, 那就是在锁你当前这个方法的对象.

并且, 注意,一定要锁住修改数据(共享资源) 的代码.

同步方法: 在方法之前加入 synchronized 就可以, 因为我们的对象的数据都是在方法中操作的,所以在方法前加 sychronized, 该方法在执行时,

  这就相当于锁 this, 但是, 当你的这个方法体中, 有其他对象的实例变量时, 它没有被锁, 这样可能会导致问题. 解决的办法是用数据块枷锁.

同步块: 个人推荐这种方法加锁, 一方面被加锁的代码范围小, 而且更加显示的说明了加锁的对象.

 

双重检测, 多线程

如果ticketNum 已经被修改成 0 了, 也就是没票了, 那么实际上, 这时就不用再监控对象来加锁了, 因为这时状态已经确定了.

但是在同步块内部, 放上相同的语句, 主要是用来监视最后 1 张票, (个人感觉影响性能不大)

上边的 if (account.money <= 0) 判断非常重要, 我们应该在程序进入同步块之前尽量的避免进入, 因为它非常影响性能.

而且不要忘了, 多线程环境下, 主程序本身也是线程之一, 它也仅仅跟其他线程是一样的.

两种锁举例: 用 12306 举例:

package com.My.Thread;

public class My12306 {
    public static void main(String[] args) {
        TicketControl tc = new TicketControl();
        new Thread(tc, "one").start();
        new Thread(tc, "two").start();
        new Thread(tc, "three").start();
    }
}

class TicketControl implements Runnable {
    private int ticketNum = 99;
    private boolean flag = true;
    
    @Override
    public void run() {
        while (flag) {
//            test1();
            test2();
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    public synchronized void test1() {
        if (ticketNum <= 0) {
            flag = false;
            return;
        } else {
            System.out.println(Thread.currentThread().getName() + "-->" + ticketNum--);    
        }
    }
    
    public void test2() {
        if (ticketNum <= 0) {
            flag = false;
            return;
        }
        synchronized(this) {
            if (ticketNum <= 0) {
                flag = false;
                return;
            }
            System.out.println(Thread.currentThread().getName() + "-->" + ticketNum--);
        }
    }
    
}

举例: 电影院

package com.My.Thread;

public class HappyCinema {

    public static void main(String[] args) {
        Cinema ci = new Cinema(20);
        String[] arrayName = {"a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9",
                "b1", "b2", "b3", "b4", "b5", "b6", "b7", "b8", "b9"};
        for(String name: arrayName) {
            new Thread(new Consumuer(ci, 2), name).start();
        }
    }

}

class Cinema{
    private int avaliable;
    
    public Cinema(int avaliable) {
        super();
        this.avaliable = avaliable;
    }
    
    public boolean bookTicket(int seat) {
        System.out.println("可用位置为:" + avaliable);
        if(this.avaliable < seat) {
            System.out.println("购票失败, 票不够了");
            return false;
        }
        this.avaliable -= seat;
        System.out.println("购票成功, 当前还剩:" + this.avaliable);
        return true;
    }
}

class Consumuer implements Runnable {
    Cinema cinema;
    private int seat;
    
    
    public Consumuer(Cinema cinema, int seat) {
        super();
        this.cinema = cinema;
        this.seat = seat;
    }

    @Override
    public void run() {
        getTicket2();
    }
    
    public synchronized void getTicket() {
        if (cinema.bookTicket(seat)){
            System.out.println(Thread.currentThread().getName() + ": getTicket");
        } else {
            System.out.println(Thread.currentThread().getName() + ": don't get");
        };
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public void getTicket2() {
        synchronized(cinema) {
            if (cinema.bookTicket(seat)){
                System.out.println(Thread.currentThread().getName() + ": getTicket");
            } else {
                System.out.println(Thread.currentThread().getName() + ": don't get");
            };
        }
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
    }

    
}

注意, 因为加锁的是对象, 所以类似 new Thread(new ObjectLock1()).start(), new Thread(new ObjectLock2()).start() 类似这种写法, 对加锁是没有意义的,

因为 ObjectLock1 和 ObjectLock2 本身就是2个对象,他们之间的成员变量是没有交集的,所以正确的写法应该是:

ObjectLock ol = new ObjectLock();    new Thread(ol, "one").start();  new Thread(ol, "two").start();

这样, 这两个线程之间才可能去访问同一个对象的成员变量, 才需要加锁.

死锁

当一个同步块中有多个对象的锁, a 线程的同步块获得了对象 X 的锁,  b 线程的同步块获得了对象 Y 的锁, 同时,a 想申请 Y 锁, b 想申请 X 锁.

解决办法: 不要在同一个同步块中出现多个对象锁, 比如

synchronized(X) {  // 表示获得了 X 锁

    synchronized(Y)  // 再其内部又想去获得 Y 锁, 不要这样. 保持同步块只锁一个对象, 把这个代码拿到 synchronized(X)的外边去.

}

正确的方式:

synchronized(X) { 修改与对象 X 有关的资源 }

synchronized(Y) { 修改与对象 Y 有关的资源 }

生产者消费者模型

 

生产者 和 消费者都是多线程, 显然中间的容器需要并发. 生产者存, 消费者取

wait() 进入等待状态(排队), 需要等待, 直到另一个线程调用该对象本身的 notify() / notifyAll() 来进行唤醒。

notify(): 唤醒正在等待对象监视器的单个线程.

notifyAll(): 唤醒所有正在等待对象监视器的所有线程.

首先, 假如说代码(class)是 x, a, b 是两个线程, 那么, a 和 b 都是按照 class 这个模子出来的线程, 现在假如在 a 和 b 内部分别调用了 this.wait(), 这时, a 和 b 这两个线程都进入了无条件长时间等待状态(释放自己占用的锁), 如果 a 线程内部有其他方法运行, 并且调用了 this.notify(), 注意这个方法在 a 线程内部, 所以这里的this实际上是指向a的,所以a 从等待状态出来, 进入排队, 准备加锁然后运行, 但是, 如果还是在线程 a 的内部, 代码是 this.notifyAll(), 表示,由这个模子刻出来的所有线程(本例中就是 a 和 b), 都可以从等待状态出来了, 进入排队状态, 然后 a 和 b 都等着获取资源(锁), 然后执行. 所以可以看到, 实际上从 wait 状态返回时, 进入等待队列(实际上也近似可以理解为进入就绪状态).

缓冲区法:

package com.My.Thread;

public class MyProCum {

    public static void main(String[] args) {
        // container 是需要加锁的资源
        ContainerPool container = new ContainerPool();
        // product & consumer 是可以通过并发启动的
        Produce product = new Produce(container);
        Consumer consumer = new Consumer(container);
        
        new Thread(product).start();
        new Thread(consumer).start();
    }
}

//生产者
class Produce implements Runnable {
    ContainerPool con;
    public Produce(ContainerPool con) {
        super();
        this.con = con;
    }
    public void doProduce() {
        for (int i = 0; i < 100; i++) {
            System.out.println("生产 -->" +i+ " salt");
            con.push(new Salt(i));
        }
    }
    @Override
    public void run() {
        doProduce();    
    }
}

//消费者
class Consumer implements Runnable {
    ContainerPool con;
    public Consumer(ContainerPool con) {
        super();
        this.con = con;
    }
    public void doConsum() {
        for (int i = 0; i < 100; i++) {
            System.out.println("消费 --> "+ con.pop().getId() +" Salt");
        }
    }
    @Override
    public void run() {
        doConsum();
    }
}
//资源池
class ContainerPool {
    Salt[] salt = new Salt[10];    // 需要加锁的资源
    int count = 0;    // 记录当前资源池的用量的栈顶
    //资源入池
    public synchronized void push(Salt s) {
        // 资源池满, 进入等待
        if (count == salt.length) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 资源池有空间
        salt[count] = s;
        count++;
        this.notifyAll();    // 释放了消费者, 同时也释放了其他生产者, 因为别人也可以生产
    }
    //资源出池
    public synchronized Salt pop() {
        // 资源池空, 等待
        if (count == 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 每次都从资源池的最上边出
        count--;
        Salt s = salt[count];
        this.notifyAll();    // 释放了生产者, 同时也释放了其他消费者, 因为有资源了,其他人也可以消费
        return s;
    }
}
//资源本身 Salt
class Salt {
    private int id;
    public Salt(int id) {
        super();
        this.id = id;
    }
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    } 
}

信号灯: 借助标志位的真假来判定

package com.My.Thread;

public class MySingleThread {

    public static void main(String[] args) {
        MyTY tv = new MyTY();
        Player p = new Player(tv);
        Listener l = new Listener(tv);
        
        new Thread(p).start();
        new Thread(l).start();
    }

}


// 生产者(演员), 多线程
class Player implements Runnable {
    MyTY tv;
    
    public Player(MyTY tv) {
        super();
        this.tv = tv;
    }

    @Override
    public void run() {
        startPlay();
    }
    
    public void startPlay() {
        for (int i = 0; i < 20; i++) {
            if (0 == i%2) {
                tv.play("Sing");
            } else {
                tv.play("Dance");
            }    
        }
    }
    
}
// 消费者(观众), 多线程
class Listener implements Runnable {
    MyTY tv;
    public Listener(MyTY tv) {
        super();
        this.tv = tv;
    }

    @Override
    public void run() {
        startWatch();
    }
    
    public void startWatch() {
        for (int i = 0; i < 20; i++) {
            // 这块必须是 20 因为前面表演了20个节目, 也必须看20个, 否则标记为不匹配
            tv.watch();    
        }
    }
    
}
// 信号灯资源, 并发资源
class MyTY {
    // 如果 flag 为真, 表示当前已经有演出了, 可以消费, 但不可以生产
    // 如果 flag 为假, 表示当前没有演出了, 可以produce, 但不可以消费
    private boolean flag = false;
    String voice;
    
    // 表演节目
    public synchronized void play(String voice) {
        // 如果是演员等待.
        if (flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 不是等待, 就执行. 注意这里不能写到 else 里. 因为当从 wait唤醒时, 还有可能继续执行这个函数的代码.
        System.out.println("表演了(生产)--> " + voice + " " + flag);
        this.voice = voice;
        flag = !flag;
        this.notifyAll();  // 因为已经生产, 所以释放消费
    }
    
    // 收听节目
    public synchronized void watch() {
        // 如果是观众等待, 就进入等待
        if (!flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
        }
        System.out.println("收听到了(消费)--> " + voice + " " + flag);
        flag = !flag;
        this.notifyAll();  // 因为已经消费, 所以释放生产
    }
}

特别注意:

wait () 这种, 函数一定要这样写:

function() {

wait()   // 触发wait

真正函数体. 这里不能把真正函数体作为wait()的对立面分支, 比如 if true -> wait, if false -> 执行代码. 这样肯定不行, 因为wait被唤醒时, 程序要继续执行, 而你把程序写成了对立分支之后, 即便wait被唤醒了,程序本身也没什么可执行的了(因为对立分支肯定走不到)

}

高级主题

Timer & QUARTZ

如果比较复杂的时间调度, 使用 QUARTZ 来调用. (需要 VPN 下载)

定时任务, Timer 和 TimerTask, 一般都是 timer.schedule(new TimerTask) 类似这种任务调度. 具体的查询 API

package com.My.Thread;

import java.util.Timer;
import java.util.TimerTask;

public class MyTimerTest {

    public static void main(String[] args) {
        Timer t = new Timer();
        t.schedule(new MyTask(), 1000);
    }
}

class MyTask extends TimerTask {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("Hello world");
        }
    }
}

ThreadLocal

 悲观锁 & 乐观锁

利用比较并交换实现 乐观锁, 乐观锁实际上没有给资源加锁, 而是在修改完成判断, 比如当前资源的版本 1 和 线程本身修改时保存的那个版本是否一致, 如果一致,证明没有线程修改过这个资源(因为修改资源后, 要把版本设置成2),这时修改成功, 然后将版本设置成2.

B如资源:

sugar 和 version, 每次线程要修改时, 将这两个变量值读入线程内部, 修改, 然后提交时, 判断当前线程保存的这个version 和 公共内存中的那个version是否一致, 如果一致(证明没有人修改过该资源),对资源进行修改,并将version 改成2. 但是这里有一个问题,因为没有给资源加锁, 所以, 如果有线程刚好做判断过后的时候对公共内存的 version 和 资源进行了修改并生效, 那么,这样就会有问题. 所以, 乐观锁是不可靠的.

比较并交换,有通过 version, 还有通过原始值来判断的,通过原始值判断的, 可能就会有 A -> B -> A 的问题, 但是通过递增的 version,就不会有这种情况.

原文地址:https://www.cnblogs.com/moveofgod/p/12456761.html