线程的生命周期
线程的声明周期共有 6 种状态,分别是:新建New、运行(可运行)Runnable、阻塞Blocked、计时等待Timed Waiting、等待Waiting和终止Terminate。
- 当你声明一个线程对象时,线程处于新建状态,系统不会为它分配资源,它只是一个空的线程对象。
- 调用start()方法时,线程就成为了可运行状态,至于是否是运行状态,则要看系统的调度了。
- 调用了sleep()方法、调用wait()方法和 IO 阻塞时,线程处于等待、计时等待或阻塞状态。
- 当run()方法执行结束后,线程也就终止了。
一、多线程的实现
Java中的Thread类就是专门用来创建线程和操作线程的类。
- 创建线程的方法:
- 继承Thread类并重写它的
run()
方法,然后用这个子类来创建对象并调用start()
方法。- 定义一个类并实现Runnable接口,实现
run()
方法。
总结:总的来说就是线程通过start()
方法启动而不是run()
方法,run()
方法的内容为我们要实现的业务逻辑。
示例:
public class ThreadStudy {
public static void main(String[] args) {
Thread1 thread1 = new Thread1();
Thread thread = new Thread(new Thread2());
thread1.start();
thread.start();
}
}
class Thread1 extends Thread {
@Override
public void run() {
super.run();
//这里我们把两个线程各自的工作设置为打印100次信息
for (int i = 0; i < 100; ++i) {
System.out.println("Hello! This is " + i);
}
}
}
class Thread2 implements Runnable {
@Override
public void run() {
//这里我们把两个线程各自的工作设置为打印100次信息
for (int i = 0; i < 100; ++i) {
System.out.println("Thanks! This is " + i);
}
}
}
二、ThreadLocal
即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
可以通过set(T)
方法来设置一个值,在当前线程下载通过get()
方法获取到原先设置的值。
示例:
private void threadLocalTest() {
Thread3 thread3 = new Thread3();
//启动两个线程
new Thread(thread3).start();
new Thread(thread3).start();
}
class Thread3 implements Runnable {
//初始化一个ThreadLocal对象,初始值为0
private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
@Override
public void run() {
for (int i = 0; i < 10; i++) {
//获取线程变量的值
Integer integer = threadLocal.get();
integer += 1;
//设置线程变量的值
threadLocal.set(integer);
System.out.println(integer);
}
}
}
三、线程同步
当多个线程操作同一个对象时,就会出现线程安全问题,被多个线程同时操作的对象数据可能会发生错误。线程同步可以保证在同一个时刻该对象只被一个线程访问。
-
Synchronized
关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它确保多个线程在同一时刻,只能有一个线程处于方法或者同步块中,保证了线程对变量访问的可见性和排他性。它有三种使用方法:
- 对普通方式使用,将会锁住当前实例对象
- 对静态方法使用,将会锁住当前类的Class对象
- 对代码块使用,将会锁住代码块中的对象
示例:
public class SynchronizedDemo {
private static Object lock = new Object();
public static void main(String[] args) {
//同步代码块 锁住lock
synchronized (lock) {
//doSomething
}
}
//静态同步方法 锁住当前类class对象
public synchronized static void staticMethod(){
}
//普通同步方法 锁住当前实例对象
public synchronized void memberMethod() {
}
}
四、Lock 与 Unlock
JUC 中的 ReentrantLock 是多线程编程中常用的加锁方式,ReentrantLock 加锁比 synchronized 加锁更加的灵活,提供了更加丰富的功能。
示例:
private static ReentrantLock lock = new ReentrantLock();
private void lockTest() {
Thread thread = new Thread(() -> {
lock.lock();
try {
System.out.println("线程1加锁");
}finally {
lock.unlock();
System.out.println("线程1解锁");
}
});
Thread thread1 = new Thread(()->{
lock.lock();
try {
System.out.println("线程2加锁");
} finally {
lock.unlock();
System.out.println("线程2解锁");
}
});
thread1.start();
thread.start();
}
五、死锁
当死锁发生时,系统将会瘫痪。比如两个线程互相等待对方释放锁。
示例:
private static Object lockA = new Object();
private static Object lockB = new Object();
public static void main(String[] args) {
//这里使用lambda表达式创建了一个线程
//线程 1
new Thread(() -> {
synchronized (lockA) {
try {
//线程休眠一段时间 确保另外一个线程可以获取到b锁
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("D");
synchronized (lockB) {
}
}
}).start();
//线程 2
new Thread(() -> {
synchronized (lockB) {
System.out.println("死锁...");
synchronized (lockA) {
}
}
}).start();
}
六、饥饿
饥饿是指一个可运行的进程尽管能继续执行,但被调度器无限期地忽视,而不能被调度执行的情况。
比如当前线程处于一个低优先级的情况下,操作系统每次都调用高优先级的线程运行,就会导致当前线程虽然可以运行,但是一直不能被运行的情况。
七、ArrayBlockingQueue
是由数组支持的有界阻塞队列。
- 构造方法:
- public ArrayBlockingQueue(int capacity):构造大小为capacity的队列
- public ArrayBlockingQueue(int capacity, boolean fair):指定队列大小,以及内部实现是公平锁还是非公平锁
- public ArrayBlockingQueue(int capacity, boolean fair, Collection<? extends E> c):指定队列大小,以及锁实现,并且在初始化是加入集合c
- 入队常用方法:
入队方法 | 队列已满 | 队列未满 |
---|---|---|
add | 抛出异常 | 返回true |
offer | 返回false | 返回true |
put | 阻塞直到插入 | 没有返回值 |
- 出队当用方法:
出队方法 | 队列为空 | 队列不为空 |
---|---|---|
remove | 抛出异常 | 移出并返回队首 |
poll | 返回null | 移出并返回队首 |
take | 阻塞直到返回 | 移出并返回队首 |
使用示例:
//构建大小为10的阻塞队列
private static ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
queue.add(i);
}
});
thread1.start();
try {
//等待线程1执行完毕
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
if (!queue.offer(11)) {
System.out.println("插入元素11失败");
}
try {
queue.put(11);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
Thread thread2 = new Thread(() -> {
Integer element;
System.out.println("开始出队:");
while ((element = queue.poll()) != null) {
System.out.print("\t" + element);
}
});
thread2.start();
}
八、LinkedBlockingQueue
LinkedBlockingQueue是一个阻塞队列,内部由两个ReentrantLock来实现出入队列的线程安全,由各自的Condition对象的await和signal来实现等待和唤醒功能。它和ArrayBlockingQueue的不同点在于:
- 队列大小有所不同,ArrayBlockingQueue是有界的初始化必须指定大小,而LinkedBlockingQueue可以是有界的也可以是无界的(Integer.MAX_VALUE),对于后者而言,当添加速度大于移除速度时,在无界的情况下,可能会造成内存溢出等问题。
- 数据存储容器不同,ArrayBlockingQueue采用的是数组作为数据存储容器,而LinkedBlockingQueue采用的则是以Node节点作为连接对象的链表。
- 由于ArrayBlockingQueue采用的是数组的存储容器,因此在插入或删除元素时不会产生或销毁任何额外的对象实例,而LinkedBlockingQueue则会生成一个额外的Node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于GC可能存在较大影响。
- 两者的实现队列添加或移除的锁不一样,ArrayBlockingQueue实现的队列中的锁是没有分离的,即添加操作和移除操作采用的同一个ReenterLock锁,而LinkedBlockingQueue实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
相关链接:http://benjaminwhx.com/2018/05/11/【细谈Java并发】谈谈LinkedBlockingQueue/
九、生成者消费者模式
生产者消费者模式是多线程编程中非常重要的设计模式,生产者负责生产数据,消费者负责消费数据。生产者消费者模式中间通常还有一个缓冲区,用于存放生产者生产的数据,而消费者则从缓冲区中获取,这样可以降低生产者和消费者之间的耦合度。 举个例子来说吧,比如有厂家,代理商,顾客,厂家就是生产者,顾客就是消费者,代理商就是缓冲区,顾客从代理商这里买东西,代理商负责从厂家处拿货,并且销售给顾客,顾客不用直接和厂家打交道,并且通过代理商,就可以直接获取商品,或者从代理商处知道货物不足,需要等待。
示例:
//阻塞队列
private static LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
public static void main(String[] args) {
//生产者
Thread provider = new Thread(() -> {
Random random = new Random();
for (int j = 0; j < 5; j++) {
try {
int i = random.nextInt();
//注释直到插入数据
queue.put(i);
System.out.println("生产数据:" + i);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//消费者
Thread consumer = new Thread(() -> {
Integer data;
for (int i = 0; i < 5; i++) {
try {
//阻塞直到取出数据
data = queue.take();
System.out.println("消费数据:" + data);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//启动线程
provider.start();
consumer.start();
}
十、线程池
线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。
- 使用Executors工具类创建
- newFixedThreadPool(int nThreads):创建一个固定大小为 n 的线程池
- newSingleThreadExecutor():创建只有一个线程的线程池
- newCachedThreadPool():创建一个根据需要创建新线程的线程池
示例:
//使用Executors 创建一个固定大小为5的线程池
private static ExecutorService executorService = Executors.newFixedThreadPool(5);
public static void main(String[] args) {
// 提交任务
executorService.submit(() -> {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
});
//停止线程池 并不会立即关闭 ,而是在线程池中的任务执行完毕后才关闭
executorService.shutdown();
}
- 直接创建线程池
示例:
private static ExecutorService executorService = new ThreadPoolExecutor(5, //核心线程数为5
10, //最大线程数为10
0L, TimeUnit.MILLISECONDS, //非核心线程存活时间
new LinkedBlockingQueue<>()); //任务队列
public static void main(String[] args) {
executorService.submit(() -> {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
});
executorService.shutdown();
}