Java基础——多线程篇

java线程的几种状态

线程和进程:
线程是进程的组成部分,一个进程可以有多个线程,一个线程必须有父进程。线程之间共享内存非常容易,所以通常,创建线程的代价小的多。

线程创建的三种方式

继承Thread类创建线程类

public class FirstThread extends Thread {
    private int i;
    public void run() {
        for( ; i < 50; i++) {
            System.out.println(getName() + " " + i);
        }
    }

    public static void main(String[] args) {
        for(var i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + "  " + i);

            if(i == 20) {
                //创建并启动第一个线程
                new FirstThread().start();
                //创建并启动第二个线程
                new FirstThread().start();
            }
        }
    }
}
  1. 定义Thread的子类,并重写该类的run方法,该类的方法体就代表了线程需要完成的任务。因此把run方法称为线程执行体。
  2. 创建了Thread子类实例就创建了线程对象。
  3. 调用线程对象的start方法来启动线程。

上面的程序中有三个线程,main, Thread-0, Thread-1, main方法决定主线程执行体。

实现Runnable接口创建线程类

  1. 定义Runnable接口的实现类,并重写改接口的run方法,这个方法的方法体同样是线程的线程执行体。
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  3. 调用start来启动该线程
public class SecondThread implements Runnable{
    private int i;
    public void run() {
        for( ; i < 50; i++)
        {
            System.out.println(Thread.currentThread().getName() + "  " + i);
        }
    }

    public static void main(String[] args) {
        for(var i =0 ; i <50; i++)
        {
            System.out.println(Thread.currentThread().getName() + "  " + i);

            if(i == 20) {
                var p = new SecondThread();
                var p2 = new SecondThread();
                new Thread(p, "_one").start();
                new Thread(p2, "_two").start();
            }
        }
    }
}

接口Runnable中只有一个抽象方法。Callable也是。上面程序中,创建了两个SecondThread实例,这是因为采用这种方式,Runnable创建的多线程可以共享线程类的实例变量。

使用CallableFuture创建线程

Callable接口是一个加强版的Runnable接口,他里面的call方法可以有返回值,可以声明抛出异常。

Future接口是用来代表Callable接口里call的返回值,提供了一个FutureTask实现类,Callable有泛型限制。

  1. 创建Callable接口的实现类,并实现call方法,在创建Callable实现类的实例。可以使用Lambda表达式来创建Callable对象。
  2. 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象call方法的返回值。
  3. 使用FutureTask对象作为Thread的target对象创建线程。
  4. 调用FutureTask兑现的get方法来获得子线程执行结束后的返回值。
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class ThridThread {
    public static void main(String[] args) {
        var tt = new ThridThread();

        FutureTask<Integer> task = new FutureTask<>((Callable<Integer>)() -> {
           var i =0;
           for( ; i < 50; i++) {
               System.out.println(Thread.currentThread().getName() + "  " + i);
           }
           return i;
        });

        for(var i = 0; i < 50; i++) {
            System.out.println(Thread.currentThread().getName() + "  " + i);
            if(i == 20) {
                new Thread(task, "Callable").start();
            }
        }
        try {
            System.out.println(task.get());
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }
}

三种方式对比

使用Runnable或者Callable接口方式创建多线程的优缺点:

  • 还可以继承其他类
  • 可以共享一个target对象,适合多个线程处理同一份资源的情况
  • 编程稍复杂

使用Thread的优缺点:

  • 不能再继承其他父类。
  • 编程简单。

一般使用的都是接口创建多线程。


线程的生命周期

新建(New),就绪(Ready),运行(Running),阻塞(Blocked),死亡(Dead)。

新建:使用new关键字创建了一个线程之后,JVM为其分配了内存。

只能对新建状态的线程调用start,否则会引发异常。

就绪:当线程对象调用了start方法之后,处于这个状态的线程并没有开始运行,只是表示可以运行了。何时开始运行取决于JVM里线程调度器的调度。

运行:当程序正在执行的时候

阻塞:程序运行时占用的内存因为调度原因被其他程序占领时。

死亡:执行结束。

什么时候未出现阻塞?

  • 线程调用sleep方法主动放弃所占用的处理资源
  • 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
  • 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有
  • 线程在某个地方正在等待通知(notify)
  • 程序调用了线程的suspend方法将线程挂起,但这个方法容易导致锁死

发生下面的情况可以解除上面的阻塞

  • 调用的sleep过了指定时间
  • 调用的阻塞IO已经返回
  • 获得了同步监听器
  • 线程正在等待某个通知时,其他线程发出了一个通知
  • 被挂起的线程被调用了resume恢复方法

线程死亡

  • run或call方法执行完成,结束后就正常结束
  • 线程出现一个未捕获的Exception或者Error
  • 直接调用该线程的stop方法来结束该线程,容易锁死

不要用start启用一个死亡的线程

线程状态转换图
Java基础——多线程篇_第1张图片

控制线程

join()让一个线程等待另一个线程执行完。

public class JoinThread extends Thread {
    public JoinThread(String name) {
        super(name);
    }
    public void run () {
        for (var i = 0; i < 100; i++) {
            System.out.println(getName() + "  " + i);
        }
    }
    public static void main(String[] args) throws Exception {
        new JoinThread("NewThread").start();
        for (var i = 0; i < 100; i++) {
            if (i ==20) {
                var jt = new JoinThread("被Join的线程");
                jt.start();

                jt.join();
            }
            System.out.println(Thread.currentThread().getName() + "  " + i);
        }
    }
}

上面代码的main线程调用用了jt的join方法,main线程处于阻塞状态,必须等jt结束才可以再继续执行。

join方法重载

  • join() 等待被join的线程执行完成
  • join(long millis) 等待被join的线程执行完成的时间最长是millis毫秒。
  • join(long millis, int nanos) 最长时间为millis + nanos(通常不用)

后台线程,在后台运行的线程,JVM的垃圾回收机制就是典型的后台线程。如果所有前台线程都死亡了,后台线程也会自动死亡。

public class DeamonThread extends Thread {
    public void run () {
        for (var i = 0; i < 500; i++) {
            System.out.println(getName() + "  " + i);
        }
    }
    
    public static void main(String[] args) {
        var t = new DeamonThread();
        //设置成后台线程
        t.setDaemon(true);
        t.start();
        for(var i = 0; i < 50; i++) {
            System.out.println(Thread.currentThread().getName() + "  " + i);
        }
    }
}

main线程结束的时候,Thread-0也结束了。

sleep()线程睡眠

  • static void sleep(long millis) 线程暂停millis毫秒,该方法收到系统计时器和线程调度器的精度与准确度的影响。
  • static void sleep(long millis, int nanos)

yield()静态方法,可以让当前正在执行的线程暂停,但他不会阻塞线程,他只是将该线程转入就绪状态。只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会获得执行的机会。所以有可能被暂停后立即执行了。

sleep和yield的区别:

  • sleep方法暂停当前线程,其他线程继续执行,yield优先级大于等于的才可以执行
  • sleep为阻塞状态,yield转为就绪
  • sleep抛出InterruptedException异常,yield没有

改变线程优先级:

通过线程的setPriority(int newPriority)来设置优先级,范围为1~10。

public class PriorityTest extends Thread{
    public PriorityTest(String name)
    {
        super(name);
    }

    public void run() {
        for(var i = 0; i < 50; i++) {
            System.out.println(getName() + ",其优先级是:" + getPriority() + ",循环变量:" + i);
        }
    }

    public static void main(String[] args) {
        Thread.currentThread().setPriority(6);
        for(var i = 0; i < 30; i++) {
            if(i == 10) {
                var low = new PriorityTest("低级");
                low.start();
                System.out.println("创建之初的优先级:" + low.getPriority());
                low.setPriority(Thread.MIN_PRIORITY);
            }
            if(i == 20) {
                var high = new PriorityTest("高级");
                high.start();
                System.out.println("创建之初的优先级:" + high.getPriority());
                high.setPriority(Thread.MAX_PRIORITY);
            }
        }
    }
}

在运行时,优先级高的有更多的执行机会。(建议使用静态常量来设置优先级)

线程同步

当有多个线程同时访问一组数据或者文件,就有很大的概率出现错误结果。这时候就需要同步监视器来解决这个问题。

同步代码块

synchronized (obj) {
    ···
    //同步代码块
}

线程开始执行同步代码块之前,必须先获得对同步监视器(obj)的锁定。

同步监视器阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器。

同步方法

public class Account {
    private String accountNo;
    private double balance;
    public Account() {}
    public Account(String accountNo, double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }
    //setter和getter

    public String getAccountNo() {
        return this.accountNo;
    }
    public double getBalance() {
        return this.balance;
    }

    public int hashCode() {
        return accountNo.hashCode();
    }

    public boolean equals(Object obj) {
        if(this == obj) {
            return true;
        }
        if(obj != null && obj.getClass() == Account.class) {
            var target = (Account) obj;
            return target.getAccountNo().equals(accountNo);
        }
        return false;
    }

    //加锁的对象是this
    public synchronized void draw(double drawAmount) {
        if (balance >= drawAmount) {
            System.out.println(Thread.currentThread().getName() + ", Successful withdraw " + drawAmount + " RMB.");
            try {
                Thread.sleep(1);
            } catch (InterruptedException ie) {
                ie.printStackTrace();
            }
            balance -= drawAmount;
            System.out.println("\r" + Thread.currentThread().getName() + " Balance:" + balance);
        } else {
            System.out.println("Sorry, " + Thread.currentThread().getName() + ", your credit is running low.");
        }
    }
}

上面例子中,直接调用account的draw方法,加锁对象是account。符合 加锁-修改-释放锁 的逻辑。

释放同步监视器

程序无法显示释放同步监视器,会在以下几种情况自动释放

  • 线程的同步方法,同步代码快执行结束
  • 遇到break,return终止该代码块或方法
  • 在代码块或方法中遇到未处理的Error或者Exception
  • 遇到同步监视器对象的wait方法,该线程暂停并释放同步监视器

在以下情况不会释放同步监视器

  • 调用线程的sleep、yield
  • 其他线程调用了该线程的suspend方法将该线程挂起

同步锁(Lock)

class x {
    //定义锁对象
    private final ReentranLock lock = new ReentranLock();
    
    //定义保证线程安全的方法
    public void m() {
        lock.lock();
        try {
            ···
        }
        finally {
            lock.unlock();
        }
    }
}

同步锁比其他两种方法更加灵活。Lock提供了用于非块结构的tryLock方法,以及试图获取可中断锁的lockInterruptibly方法,还有超时失效锁的tryLock(long, TimeUnit)方法。

ReentrantLock锁具有可重入性,也就是说,一个线程可以对已被加锁的ReentrantLock锁再次加锁。

一段被锁保护的代码,可以调用另一个被相同锁保护的方法。

死锁及常用出路策略

当两个线程相互等待对方释放同步监视器时就会发生死锁。程序不会抛出任何异常,也没有提示,所有线程一直处于阻塞状态。

class A {
    public synchronized void foo(B b) {
        System.out.println("当前线程名称:" + Thread.currentThread().getName() + "进入了A实例的foo方法");
        try {
            Thread.sleep(200);
        }
        catch (InterruptedException ie) {
            ie.printStackTrace();
        }
        System.out.println("当前线程名称:" + Thread.currentThread().getName() + "试图调用B实例的last方法");
        b.last();
    }

    public synchronized void last() {
        System.out.println("进入了A类的last方法内部");
    }
}

class B {
    public synchronized void bar(A a) {
        System.out.println("当前线程名称:" + Thread.currentThread().getName() + "进入了B实例的foo方法");
        try {
            Thread.sleep(200);
        }
        catch (InterruptedException ie) {
            ie.printStackTrace();
        }
        System.out.println("当前线程名称:" + Thread.currentThread().getName() + "试图调用A实例的last方法");
        a.last();
    }

    public synchronized void last() {
        System.out.println("进入了A类的last方法内部");
    }
}

public class DeadLock implements Runnable{
    A a = new A();
    B b = new B();
    public void init() {
        Thread.currentThread().setName("主线程");
        a.foo(b);
        System.out.println("进入主线程后···");
    }

    public void run() {
        Thread.currentThread().setName("副线程");
        b.bar(a);
        System.out.println("进入副线程后···");
    }

    public static void main(String[] args) {
        var dl = new DeadLock();
        new Thread(dl).start();
        dl.init();
    }

}

出现下面的结果

当前线程名称:主线程进入了A实例的foo方法
当前线程名称:副线程进入了B实例的foo方法
当前线程名称:主线程试图调用B实例的last方法
当前线程名称:副线程试图调用A实例的last方法

程序无法向下执行了。是因为两个线程都在等待对方把锁解开。

应该尽量避免这种情况的发生

  • 尽量避免同一个线程对多个同步监视器进行锁定
  • 使用定时锁tryLock

线程通信

传统的线程通信,使用wait()notify()notifyAll()来通信,这三通方法由同步监视器来调用。第一个是让当前线程等待,直到另外两个方法来将他唤醒。另外两个就是唤醒在等待中的线程的,第二个随机唤醒一个,第三个是唤醒所有。

使用Condition来控制线程通信

如果程序不使用synchronized关键字来保证同步,而是用Lock,就不能使用传统的了。获得一个Condition对象,需要调用Lock对象的newCondition方法。

Condition提供了三个方法:

方法名 介绍
await() 类似于wait。有很多变体
signal() 唤醒单个在等待的线程,也是任意的。只有当前线程放弃了对该Lock对象的锁定后(使用await),才可执行被唤醒的线程。
signalAll() 唤醒所有在等待中的线程,只有当前线程放弃了对该Lock对象的锁定后,才可执行被唤醒的线程。
public class Account {
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    private String accountNo;
    private double balance;
    private boolean flag = false;
    public Account() {};
    public Account(String accountNo, double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }

    public String getAccountNo() {
        return accountNo;
    }

    public void draw(double drawAmount) {
        lock.lock();
        try {
            if (!flag) {
                condition.await();
            }
            else {
                if (!(drawAmount > balance)) {
                    System.out.println(Thread.currentThread().getName() + " 取钱:" + drawAmount);
                    balance -= drawAmount;
                    System.out.println("账户余额为:" + balance);
                }
                flag = false;
                condition.signalAll();
            }
        }
        catch (InterruptedException ie) {
            ie.printStackTrace();
        }
        finally {
            lock.unlock();
        }
    }

    public void deposit(double depositAmount) {
        lock.lock();
        try {
            if (flag) {
                wait();
            }
            else {
                System.out.println(Thread.currentThread().getName() + " 存钱:" + depositAmount);
                balance += depositAmount;
                System.out.println("账户余额为:" + balance);

                flag = true;
                condition.signalAll();
            }
        }
        catch (InterruptedException ie) {
            ie.printStackTrace();
        }
        finally {
            lock.unlock();
        }
    }

    public int hashCode() {
        return accountNo.hashCode();
    }

    public boolean equals(Object obj) {
        if(this == obj) {
            return true;
        }
        if(obj != null && obj.getClass() == S16_6.syn.Account.class) {
            var target = (S16_6.syn.Account) obj;
            return target.getAccountNo().equals(accountNo);
        }
        return false;
    }
}

Condition与传统的相比逻辑基本相似。

使用阻塞队列(BlockingQueue)控制线程通信

- 抛出异常 不同返回值 阻塞线程 制定超时时长
队尾插入元素 add(e) offer(e) put(e) offer(e, time, unit)
队头删除元素 remove() poll() take() poll(time, unit)
获取,不删除元素 elemrnt() peek()

BlockingQueue与其实现类

Deque
Queue
BlockingDeque
BlockingQueue
LinkedBlockingDeque
LinkedTransferQueue
TransferQueue
DelayQueue
LinkedBlockingQueue
ArrayBlockingQueue
SynchronousQueue
PriorityBlockingQueue

下面代码利用BlockingQueue来实现线程通信

class Producer extends Thread {
    private BlockingQueue<String> bq;
    public Producer(BlockingQueue<String> bq) {
        this.bq = bq;
    }
    public void run() {
        var strArr = new String[]{"java", "abc", "xyz"};
        for (var i = 0; i < 999999; i ++) {
            System.out.println(getName() + " 生产者准备生产集合元素");
            try {
                Thread.sleep(200);
                bq.put(strArr[i % 3]);
            }
            catch(Exception e) {e.printStackTrace();}
            System.out.println(getName() + "生产完成:" + bq);
        }
    }
}
class Consumer extends Thread {
    private BlockingQueue<String> bq;
    public Consumer(BlockingQueue<String> bq) {this.bq = bq;}
    public void run() {
        while(true) {
            System.out.println(getName() + "消费者准备消费集合元素!");
            try {
                Thread.sleep(200);
                bq.take();
            }
            catch (Exception e) {e.printStackTrace();}
            System.out.println(getName() + "消费完成:" + bq);
        }
    }
}
public class BlockingQueueTest {
    public static void main(String[] args) {
        BlockingQueue<String> bq = new ArrayBlockingQueue<>(1);
        new Producer(bq).start();
        new Producer(bq).start();
        new Producer(bq).start();
        new Consumer(bq).start();
    }
}

运行后,三个线程都想往队列里放元素,但是只由1个空间,某个线程放入了后,其他放入元素的线程就只能等待元素被取出。

线程组合未处理的异常

java使用ThreadGroup来表示线程组。对线程组的控制相当于同时控制这批线程。用户创建的线程都属于制定的线程,没有制定线程组就属于默认线程组。线程创建的线程属于它本身的线程组。线程运行中途不能改变所属线程组。

下面的代码示范了线程组的一些用法。

class MyThread extends Thread {
    //提供制定线程名的构造器
    public MyThread(String name) {
        super(name);
    }
    //提供制定线程名、线程组的构造器
    public MyThread(ThreadGroup group, String name) {
        super(group, name);
    }

    public void run() {
        for(var i = 0; i < 20; i ++) {
            System.out.println(getName() + " 线程的i变量" + i);
        }
    }
}

public class ThreadGroupTest {
    public static void main(String[] args) {
        //获取主线程所在的线程组,这是所有线程默认的线程组
        ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
        System.out.println("主线程的名字:" + mainGroup.getName());
        System.out.println("主线程是否是后台线程:" + mainGroup.isDaemon());

        new MyThread("主线程的线程").start();

        var tg = new ThreadGroup("新线程组");
        tg.setDaemon(true);
        System.out.println("tg线程组是否是后台线程组:" + tg.isDaemon());
        var tt = new MyThread(tg, "tg组的线程甲");
        tt.start();
        new MyThread(tg, "tg组的线程乙").start();
    }
}

如果线程执行过程中抛出了一个未处理的异常,jvm在结束该线程之前会自动检查是否有对应的 Thread.UncaughtExceptionHandler 对象,如果有,会调用 void uncaughtException(Thread t, Throwable e)。如果没有找到,JVM会调用该线程所属的线程组的 uncaughtException() 方法来处理该异常。

class MyHandler implements Thread.UncaughtExceptionHandler{
    //实现uncaughtException方法,该方法将处理线程未处理的异常
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println(t + " 线程出现了异常 : "  + e);
    }
}

public class ExHandler{
    public static void main(String[] args) {
        Thread.currentThread().setUncaughtExceptionHandler(new MyHandler());
        var a = 5/0;
        System.out.println("program exit.");
    }
}

运行结果:

Thread[main,5,main] 线程出现了异常 : java.lang.ArithmeticException: / by zero

可以发现,并没有执行最后一句输出命令,程序没有正常结束。

线程池

(1)、降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;

(2)、提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;

(3)方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。

(4)提供更强大的功能,延时定时线程池。


Executors工厂类生产线程池:

  • newCachedThreadPool():
  • newFixedThreadPool(int nThreads):
  • newSingleThreadPool():
  • newScheduledThreadPool(int corePoolSize):
  • newSingleThreadScheduledExecutor():
  • ExecutorService newWorkStealingPool(int parallrlism):
  • ExecutorService newWorkStealingPool():

前三个返回一个 ExecutorService 对象,代表一个线程池,可以执行 Runnable 对象或 Callable 对象所代表的线程;中间两个返回 ScheduledExecutorService 线程池,他是 ExecutorService 的子类,可以再制定延迟后执行线程任务。最后两个是用于并行。

ExecutorService 代表了尽快执行的线程池,提供了三种方法:

  • Future submit(Runnable task)
  • future submit(Runnable task, T result)
  • Futuresubmit(Callable task)

ScheduledExecutorService 也提供了类似的方法,具体看API

执行完后,用 shutdown() 或 shutdownNow() 来关闭,不会再接受新的任务,但是第一个方法会让原有的执行完,第二个会试图停止所有的任务,并返回等待执行的任务列表。

使用线程池来执行线程任务的步骤:

  1. 调用 Executors 类的静态工厂方法创建一个 ExecutorService 对象,该对象代表一个线程池。
  2. 创建 Runnable 实现类或 Callable 实现类的实例,作为线程执行任务。
  3. 调用 ExecutorService 对象的 submit() 方法来提交 Runnable 实例或 Callable 实例。
  4. 可以调用 shutdown 来关闭线程池。
public class ThreadPoolTest {
    public static void main(String[] args) throws Exception{
        ExecutorService pool = Executors.newFixedThreadPool(6);
        Runnable target = ()-> {
            for(var i=0;i<50;i++){
               System.out.println(Thread.currentThread().getName() + " 的 i 值为:" + i);
            }
        };
        pool.submit(target);
        pool.submit(target);
        pool.shutdown();
    }
}

使用 ForkJoinPool 利用多cpu

RecursiveAction
ForkJoinTask
RecursiveTask
Future
ForkJoinPool
AbstractExecutorService
ExecutorService
Executor

创建 ForkJoinPool 实例之后,就可以调用 ForkJoinPool 的 submit(ForkJoinTesk task) 或 invoke(ForkJoinTask task) 方法来执行制定任务了。
其中 ForkJoinTask 代表一个可以并行、合并的任务。
ForkJoinTask 是一个抽象类,有两个子类,如上。RecursiveTask 代表有返回值的任务,另一个代表的是无返回值的。

class CalTask extends RecursiveTask<Integer>
{
    //每个小任务最多只累加20个数
    private static final int THRESHOLD = 20;
    private int[] arr;
    private int start;
    private int end;
    public CalTask(int[] arr, int start, int end) {
        this.arr = arr;
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum = 0;
        if(end-start < THRESHOLD) {
            for(var i = start; i <end; i++){
                sum+=arr[i];
            }
            return sum;
        }
        else {
            //拆分
            int middle = (start + end)/2;
            var left=new CalTask(arr, start, middle);
            var right=new CalTask(arr,middle,end);
            left.fork();
            right.fork();
            return left.join()+right.join();
        }
    }
}

public class Sum {
    public static void main(String[] args) throws Exception{
        var arr=new int[100];
        var rand=new Random();
        var total=0;
        for(int i=0,len=arr.length;i<len;i++){
            int tmp=rand.nextInt(20);
            //求出总和并赋值
            total+=(arr[i]=tmp);
        }
        System.out.println(total);

        ForkJoinPool pool = ForkJoinPool.commonPool();
        Future<Integer> future=pool.submit(new CalTask(arr,0,arr.length));

        System.out.println(future.get());
        pool.shutdown();
    }
}

其他

线程相关类

1.ThreadLocal 类

为每个线程都创建一个变量副本,以免造成冲突。提供了三个 public 方法。

  • T get(): 返回此线程局部变量中当前线程副本中的值。
  • void remove(): 删除此线程局部变量中当前线程的值。
  • void set(T value): 设置此线程局部变量中当前线程副本中的值。
class Account {
    private ThreadLocal<String> name = new ThreadLocal<>();
    public Account(String str) {
        this.name.set(str);
        System.out.println("---" + this.name.get());
    }
    public String getName() {return name.get();}

    public void setName(String str) {
        this.name.set(str);
    }
}

class MyTest extends Thread{
    private Account account;
    public MyTest(Account acc, String name) {
        super(name);
        this.account = acc;
    }
    public void run() {
        for(var i = 0; i < 10; i++) {
            if(i==6){
                account.setName(getName());
            }
            System.out.println(account.getName() + "账户的i值" + i);
        }
    }
}

public class ThreadLocalTest {
    public static void main(String[] args) {
        var at = new Account("初始名");
        new MyTest(at, "Thread-First").start();
        new MyTest(at, "Thread-Second").start();
    }
}

总共有三个线程,每个线程都完全拥有自己的ThreadLocal变量。
改变的变量也是自己的。

2.包装线程不安全的集合

java 集合比如:ArrayList,LinkedList,HashSet,TreeSet,HashMap,TreeMap等都是线程不安全的。

Collections 提供了几个方法可以吧他们包装成线程安全的集合。

  • Collection synchronizedCollection(Collection c)
  • static List synchronizedList(List list)
  • static Map synchronizedMap(Map m)
  • static Set synchronizedSet(Set s)
  • static SortedMap synchronizedSortedMap(SortedMap m)
  • static SortedSet synchronizedSortedSet(SortedSet s)

上面的都是返回特定的线程安全的对象

//讲HashMap包装成线程安全的类
HashMap m = Collections.synchronizedMap(new HashMap());
线程安全的集合类
ConcurrentSkipListMap
ConcurrentNavigableMap
NavigableMap
ConcurrentMap
ConcurrentHashMap
Map
ConcurrentLinkedDeque
Deque
Queue
ConcurrentLinkedQueue
CopyOnWriteArraySet
Set
ConcurrentSkipListSet
CopyOnWriteArrayList
List
Collection

主要分为两类:

  1. Concurrent 开头的,代表支持并发访问的集合,他们可以支持多个线程并发写入访问,使用了更复杂的算法保证不被锁定。有较好的性能
  2. CopyOnWrite 开头的,采用复制底层数组的方式来实现写操作。性能较差,适合用在读取操作上。

3.发布订阅框架

public class PubSubTest {
    public static void main(String[] args) {
        //创建一个SubmissionPublisher作为发布者
        SubmissionPublisher<String> publisher = new SubmissionPublisher<>();
        //创建订阅者
        MySubscriber<String> subscriber = new MySubscriber<>();
        //注册订阅者
        publisher.subscribe(subscriber);
        //发布几个数据项
        System.out.println("开发发布数据···");
        List.of("Java", "Kotlin", "Go", "Erlang", "Swift", "Lua").forEach(im -> {
            publisher.submit(im);
            try{
                Thread.sleep(500);
            }
            catch (Exception ignored) {}
        });
        publisher.close();
        synchronized ("fkjava"){
            try{
                "fkjava".wait();
            }
            catch (Exception ignored) {}
        }
    }
}

class MySubscriber<T> implements Subscriber<T> {
    private Flow.Subscription subscription;
    @Override
    public void onSubscribe(Flow.Subscription subscription){
        this.subscription = subscription;
        subscription.request(1);
    }

    @Override
    public void onNext(T item) {
        System.out.println("获取到数据:" + item);
        subscription.request(1);
    }

    @Override
    public void onError(Throwable t){
        t.printStackTrace();
        synchronized ("fkjava") {
            "fkjava".notifyAll();
        }
    }

    @Override
    public void onComplete() {
        System.out.println("订阅结束!");
        synchronized ("fkjava") {
            "fkjava".notifyAll();
        }
    }
}

运行结果:

开发发布数据···
获取到数据:Java
获取到数据:Kotlin
获取到数据:Go
获取到数据:Erlang
获取到数据:Swift
获取到数据:Lua
订阅结束!

你可能感兴趣的:(Java)