【Java】Java 多线程

一、Java 多线程

Java中操作多线程最核心的类Thread,其被放置在java.lang下不需要我们手动导入

1.1 Java 线程基本属性

image-20230930174455514

  • 名称:构造方法里起的名字

  • 线程状态:Java中的线程的状态要比操作系统原生的状态更加丰富一些

  • 优先级:可以获取和设置,但是没有啥用,只是给操作系统建议

  • 是否后台线程:前台线程会阻止进程结束,前台线程的工作没有做完,进程是无法结束的,后台线程,不会阻止进程结束,后台线程的工作没有做完,进程也是可以结束的

  • 是否存活:判断系统里该线程是否工作

  • 是否中断:通知线程,你应该停止了(建议)

    • interrupt会做两件事:

      1、吧线程内部标识位设置成true

      2、如果线程在sleep,就会触发异常,把线程唤醒。但是sleep在唤醒的时候,还会将刚才的中断标识位重新设置成false(清空标志位)

      sleep重制标志位的意义:唤醒之后,线程到底要终止还是继续,到底是立即终止还是稍后终止,就把选择权交给了程序员

      package thread;
      
      public class ThreadDemo8 {
          public static void main(String[] args) {
              Thread t = new Thread(() -> {
                  // currentThread()(静态成员方法)可以获取到线程对象的引用
                  System.out.println("hello world");
                  while (!Thread.currentThread().isInterrupted()) {
                      try {
                          Thread.sleep(1000);
                      } catch (InterruptedException e) {
                          e.printStackTrace  ();
                        // break(); // 立即响应终止请求
                      }
                  }
              }, "myThread");
              t.start();
      
              try {
                  Thread.sleep(3000);
                // 告诉线程,你应该终止了
                  t.interrupt();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      }
      

image-20230930173631505

1.2 创建线程的五种方式

1、实现Callable接口

当我们需要创建一个可以返回结果的线程时,就可以使用实现了Callable接口的方式。Callable接口是在Java 5中引入的,它允许线程执行一个任务并返回一个结果,与Runnable接口相比,Callable接口的call()方法可以返回结果并抛出异常。

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
 
class MyCallable implements Callable<String> {
    public String call() {
        return "Thread is running...";
    }
}
 
public class Main {
    public static void main(String[] args) {
        // 创建Callable实现类的实例
        Callable<String> callable = new MyCallable();
        
        // 创建FutureTask对象,用于包装Callable对象
        FutureTask<String> futureTask = new FutureTask<>(callable);
        
        // 创建线程并启动
        Thread thread = new Thread(futureTask);
        thread.start();
        
        try {
            // 获取线程执行结果
            String result = futureTask.get();
            System.out.println(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Callable 和 Runnable的最大区别就是前者接口中的方法是有返回值的,并且Callable接口需要通过FutureTask方法封装后进行使用,还有以下使用方法

package thread;


import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadDemo29 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> callable = new Callable<Integer>() {
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 1; i <= 1000; i++) {
                    sum += i;
                }
                return sum;
            }
        };
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t1 = new Thread(futureTask);
        Thread t2 = new Thread(new FutureTask<Integer>(new Callable<Integer>() {
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 1; i <= 1000; i++) {
                    sum += i;
                }
                return sum;
            }
        }));
        t1.start();
        t2.start();

        // get 方法获取结果,如果线程没有执行完get方法会被阻塞
        Integer result = futureTask.get();
        System.out.println(result);
    }

}

2、实现Runnable 接口

// Runnable 作用,是描述一个需要执行的任务,run方法就是执行细节
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("hello thread");
    }
}

public class ThreadDemo2 {
    public static void main(String[] args) {
        Runnable runnable = new MyRunnable();
        Thread t = new Thread(runnable);
        t.start();
    }
}

3、使用匿名内部类

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            public void run() {
                System.out.println("Thread is running...");
            }
        });
        thread.start(); // 启动线程
    }
}

4、继承Thread

可以通过继承Thread类来创建线程,然后重写run()方法来定义线程执行的任务。

class MyThread extends Thread {
    public void run() {
        System.out.println("Thread is running...");
    }
}
 
public class Main {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start(); // 启动线程
    }
}

5、使用lambda表达式

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("Thread is running...");
        });
        thread.start(); // 启动线程
    }
}
1.3 Java 线程等待

线程是一个随机调度的过程,等待线程做的事就是控制两个线程的结束顺序

image-20230930184328526

package thread;

public class ThreadDemo9 {
    public static void main(String[] args) throws InterruptedException {

        Thread t = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        t.start();

        try {
            t.join();
            System.out.println("child quit");
            System.out.println("I'm quit");

        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}
1.4 Java 线程状态
package thread;

public class ThreadDemo10 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 10000000; i++) {

            }
        });

        System.out.println("start 之前:" + t.getState());
        t.start();

        try {
        	  System.out.println("start 之后:" + t.getState());
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 执行完毕之后,就是TERMINATED状态
        System.out.println("t 结束之后:" + t.getState());
    }
}

线程对象只能start一次,不能重复start

TERMINATED状态:内核中的线程PCB消亡了,此时代码中变量t就没啥用了,而Java中对象的生命周期由Java自己控制,此时就需要特定的状态,来把t对象标识成无效的

1.5 多线程的优点
package thread;

public class ThreadDemo11 {
    public static void main(String[] args) {
        serial1();
        serial2();
    }

    public static void serial2() {
        // 为了衡量执行速度,加上一个计时操作
        // currentTimeMillis 获取到当前系统的 ms 级时间戳

        long beg = System.currentTimeMillis(); // 毫秒级时间戳

        long a = 0;
        for (long i = 0; i < 100_0000_0000L; i++) {
            a++;
        }

        long b = 0;
        for (long i = 0; i < 100_0000_0000L; i++) {
            b++;
        }

        long end = System.currentTimeMillis();
        System.out.println("执行时间:" + (end - beg) + "ms");
    }

    public static void serial1() {
        // 为了衡量执行速度,加上一个计时操作
        // currentTimeMillis 获取到当前系统的 ms 级时间戳

        long beg = System.currentTimeMillis(); // 毫秒级时间戳

        Thread t1 = new Thread(() -> {
            long a = 0;
            for (long i = 0; i < 100_0000_0000L; i++) {
                a++;
            }
        });

        Thread t2 = new Thread(() -> {
            long b = 0;
            for (long i = 0; i < 100_0000_0000L; i++) {
                b++;
            }
        });
        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        long end = System.currentTimeMillis();
        System.out.println("执行时间:" + (end - beg) + "ms");
    }
}
1.6 Java标准库中的线程安全类

线程不安全的

这些类在多线程代码中使用需要格外注意

ArrayList 、LinkedList、HashMap、TreeMap、HashSet、TreeSet、StringBuilder

线程安全的

已经内置了syncronized加锁,相对来说安全一点

Vector(不推荐使用),HashTable(不推荐使用),ConcurrentHashMap,StringBuffer

1.7 保证线程安全的方法

使用synchronized 直接对this指针进行加锁

public synchronized void add() {
        count++;
}
1.8 Java 实现自定义类型比较器

方法1:给自定义类型实继承Comparable接口,实现虚接口compareTo()

package thread;
import java.util.concurrent.PriorityBlockingQueue;

class MyTask implements Comparable<MyTask> {
    public MyTask(Runnable runnable, long time) {
        this.runnable = runnable;
        this.time = time;
    }

    public long getTime() {
        return time;
    }

    public void run() {
        runnable.run();
    }

    private Runnable runnable;
    private long time;

    @Override
    public int compareTo(MyTask o) {
        return (int)(this.time - o.time);
    }
}

class MyTimer {
    // 扫描线程
    private Thread t = null;
    // 有一个阻塞优先级队列用来保存队列
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    public MyTimer() {
        t = new Thread(() -> {
            while (true) {
                // 取出队首元素,检查队首元素是否到时间了
                try {
                    MyTask myTask = queue.take();
                    long curTime = System.currentTimeMillis();
                    if (curTime < myTask.getTime()) {
                        // 如果时间没到,就把任务塞回到队列中去
                        queue.put(myTask);
                        synchronized(this) {
                            this.wait(myTask.getTime() - curTime);
                        }
                    } else {
                        // 如果时间到了,就把任务进行执行
                        myTask.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }

    // 第一个参数是任务内容,第二个参数是任务多久后执行
    public void schedule(Runnable runnable, long after) {
        // 注意这里时间进行换算
        MyTask task = new MyTask(runnable, System.currentTimeMillis() + after);
        queue.put(task);
        synchronized (this) {
            this.notify();
        }
    }
}

public class ThreadDemo25 {
    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();

        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务1:");
            }
        }, 1000);

        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务1:");
            }
        }, 2000);
    }
}

方法2:使用Comparator单独写个比较器

二、Java 线程池

标准库中的线程池

// 简单构建一个线程池
// 创建一个线程池,线程里线程数目固定10个
ExecutorService pool1 = Executors.newFixedThreadPool(10);

image-20231003121655229

  • newCachedThreadPool : 线程动态变化的线程池
  • newFIxedThreadPool: 固定线程数量的线程池
  • newSingleThreadPool : 只有一个线程的线程池
  • newScheduledThreadPool: 类似于定时器,让任务延时执行,执行时并非由扫描线程执行,而是由单独的线程池进行执行

上述的线程池都是通过包装ThreadPoolExecutor实现的,使用工厂设计模式使用更加方便

2.1 ThreadPoolExecutor

文档网址:https://docs.oracle.com/javase/8/docs/api/index.html

image-20231003123206014

ThreadPoolExecutor(
    int corePoolSize, 
    int maximumPoolSize, 
    long keepAliveTime, 
    TimeUnit unit, 
    BlockingQueue<Runnable> workQueue, 
    ThreadFactory threadFactory, 
    RejectedExecutionHandler handler
)
  • corePoolSize:核心线程数
  • maximumPoolSize: 最大线程数
  • workQueue : 线程池的任务队列(阻塞队列,每个工作线程take成功执行任务,失败,进入阻塞)
  • threadFactory : 线程工厂,帮助线程池创建线程
  • handler:拒绝策略,当任务队列满了后再添加任务需要进行判断

解释:

核心线程:即线程池中工作的正式员工线程,其余的是临时线程(临时工),两者区别就是临时工摸鱼会被销毁

拒绝策略:以下是标准库提供的四种拒绝策略

image-20231003124224581

  • ThreadPoolExecutor.AbortPolicy: 队列满了再插入,就抛异常
  • ThreadPoolExecutor.CallerRunsPolicy:队列满了再插入,谁加的谁执行
  • ThreadPoolExecutor.DiscardOldestPolicy:如果队列满了,丢弃最早的任务
  • ThreadPoolExecutor.DiscardPolicy:丢弃最新的任务
2.2 Java简易线程池的实现
package thread;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

//
class MyThreadPool {
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();

    public MyThreadPool(int n) {
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(() -> {
                while (true) {
                    Runnable runnable = null;
                    try {
                        runnable = queue.take();
                        runnable.run();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            t.start();
        }
    }

    public void submit(Runnable runnable) {
        try {
            queue.put(runnable);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class ThreadDemo27 {
    public static void main(String[] args) {
        MyThreadPool pool = new MyThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            int n = i;
            pool.submit(new Runnable() {

                public void run() {
                    System.out.println("hello " + n);
                }
            });
        }
    }
}

三、Java 线程安全

3.1 Java 原子类

源自类内部使用CAS方法实现,所以性能会比加锁操作高很多,标准库中提供了AtomicBoolean,AtomicInteger,AtomicIntegerArray,AtomicLong,AtomicReference,AtomicStampedReference几个类

CAS存在ABA问题,自旋时间过长,以及线程安全的范围不能灵活控制等问题

原子类的原理将用以下伪代码进行解释:

这里CAS本身修改value,不过CAS需要判定value和oldValue是否相等,不相等就重新进行读取

class AtomicInteger {
		private int value;
		
		public int getAndIncrement() {
      // oldValue可以理解寄存器中的值,将内存的值读取到寄存器中
		int oldValue = value;
    // 正常情况下oldValue和value相同,但是也可能另外一个线程修改value值,所以进行CAS判定,如果value值发生了变化则重新读取value的值,然后对其进行操作
				while (CAS(value, oldValue, oldValue + 1) != true) {
						oldValue = value;
				}
				return oldValue;
		}
}
package thread;

import java.util.concurrent.atomic.AtomicInteger;

public class ThreadDemo28 {
    public static void main(String[] args) {

        AtomicInteger count = new AtomicInteger(0);

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count.incrementAndGet();
                count.getAndIncrement();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count.incrementAndGet();
                count.getAndIncrement();
            }
        });

        try {
            t1.start();
            t2.start();
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println(count);
    }
}

3.2 SpinLock 自旋锁

监测当前的owner是否为null,如果为null将当前线程的引用赋值给owner,如果赋值成功,循环结束,加锁完成

如果当前锁被其他线程占用,CAS就会发现this.owner不是null,CS就不会产生赋值操作,同时返回false,循环继续执行,进行下次判定

public class SpinLock {
		private Thread owner = null;
		public void lock() {
				 while (!CAS(this.owner, null, Thread.currentThread())) {
				 
				 }
		}
		public void unlock() {
			this.owner = null;
		}
}
3.3 ReentrantLock

相比于synchronized,ReentrantLock使用方法更复古,手动调用lock和unlock进行加锁解锁

使用方法

package thread;

import java.util.concurrent.locks.ReentrantLock;

public class ThreadDemo30 {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();

        try {
            reentrantLock.lock();
            if (coud1) {
                return;
            }

            if (coud2) {
                return;
            }
            throw new Exception();
        } finally {
            reentrantLock.unlock();
        }
    }
}

手动unlock确实带来了很多不便的地方,但是ReentrantLock也是有优势的

  • ReentrantLock 提供了公平锁版本的实现

    // 添加参数true提供公平锁
    ReentrantLock reentrantLock = new ReentrantLock(true);
    
  • 对于synchronized来说,加锁操作就是死等,只要获取不到锁就会一直等待,但是ReentrantLock提供了更加灵活的等待方式,tryLock,无参数版本,能加锁就加锁,不能就放弃。有参数版本,等一段时间加上就算了,没有就放弃

    reentrantLock.trylock();
    
  • ReentrantLock还搭配了一个Condition类,进行唤醒的时候可以指定唤醒线程

但是虽然ReentrantLock功能更加强大,但是使用方法较为复杂,工作中使用的较少

3.4 Semaphore 信号量

和Linux的信号量类似,使用acquire方法申请信号量(P操作),使用release方法释放信号量(V操作)

package thread;

import java.util.concurrent.Semaphore;

public class ThreadDemo31 {
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(3);
        semaphore.acquire();
        System.out.println("执行一次P操作");
        semaphore.acquire();
        System.out.println("执行一次P操作");
        semaphore.acquire();
        System.out.println("执行一次P操作");
        semaphore.release();
        System.out.println("执行一次V操作");
        semaphore.acquire();
        System.out.println("执行一次P操作");
    }
}

四、多线程环境下Java标准库使用

Java标准库中大部分集合类都是线程不安全的,多个线程使用同一个集合类对象可能会出问题

Vector,Stack,HashTable这几个类是少数线程安全的集合类,但是其是无脑加锁的,不推荐使用

多线程环境使用ArrayList

1、自己加锁,使用synchronized 或者 ReentrantLock 进行加锁

2、Collections.synchronizedList 这里会提供一些ArrayList相关方法,同时是带锁的,使用这个方法把集合类套一层

3、CopyOnWriteArrayList 简称COW也叫做写时拷贝,如果针对ArrayList进行读取操作,不需要做任何工作,如果进行写操作,就需要拷贝一份新的ArrayList进行修改,修改过程中只能读取旧的,修改完毕后就用新的替换旧的

多线程使用哈希表

HashMap线程不安全,但是HashTable线程安全,但是更推荐使用ConcurrentHashMap更优化的线程安全哈希表

1、最大优化之处,就是相对于HashTable大大缩小了锁冲突的概率,把一把大锁替换成了很多小锁(分段锁)

2、只针对写进行加锁,读和写没有冲突,使用代码控制写操作是原子的

3、内部充分使用CAS,通过这个来进一步削减加锁操作的数目

4、针对扩容,采取化整为零的方式,创建一个更大的数组空间,但是保留旧的数组,每次进行put操作都往新数组添加的同时也搬运一小部分元素

  • 29
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白在进击

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值