多线程
基本概念
并发与并行
并发:两个或多个事件在同一时间段发生
并行:两个或多个事件在同一时刻发生(同时发生)
程序、进程、线程
程序:是为完成特定任务,用某种语言编写的一组指令的集合,即指一段静态的代码,静态对象。
进程:是程序的一次执行过程,或是正在运行的一个程序,是一个动态的过程,有它自身的产生,存在和消亡的过程。-------生命周期
线程:进程可进一步细化为线程,是一个程序内部的一条执行路径
线程分类
java中的线程分为两类:
- 守护线程(如垃圾回收线程,异常处理线程)
- 用户线程(如主线程)
- Java创建的线程默认是用户线程。
两者的差别是,当进程中还有用户线程在运行时,进程不终止;当进程中只有守护线程在运行时,进程终止。
线程的生命周期
创建多线程
继承Thread类创建线程
一、步骤
1.写一个类直接继承Thread类
2.重写run方法.该run()方法的方法体就代表了线程需要完成的任务
3.创建Thread子类的实例
4.调用线程对象的start()方法来启动该线程
public class MyThread extends Thread {
@Override
public void run() {
}
}
public class ThreadExample {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
}
}
实现Runnable接口创建线程
一、步骤
1.定义Runnable的实现类,重写run()方法
2.创建Runnable实现类的实例,并以此作为Thread的target来创建对象,该对象才是真正的线程对象
public class MyRunnable implements Runnable{
@Override
public void run() {
}
}
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("这个是实现Runnable");
}
}
public class ThreadExample {
public static void main(String[] args) {
// 第一步
Runnable runable = new MyRunnable();
//第二步
Thread thread = new Thread(runable);
// 第三步
thread.start();
}
}
使用Runnable的优点:
- 适合多个相同的程序代码的线程去处理同一个资源
- 可以避免java中的单继承的限制
- 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
- 线程池只能放入实现Runnable或callable类线程,不能直接放入继承Thread的类
使用线程池创建(使用java.util.concurrent.Executor接口)
步骤
- 需要创建实现runnable或者callable接口方式的对象
- 创建executorservice线程池
- 将创建好的实现了runnable接口类的对象放入executorService对象的execute方法中执行。
- 关闭线程池
package com.example.paoduantui.Thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class NumberThread implements Runnable{
@Override
public void run() {
for(int i = 0;i<=100;i++){
if (i % 2 ==0 )
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
class NumberThread1 implements Runnable{
@Override
public void run() {
for(int i = 0;i<100; i++){
if(i%2==1){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args){
//创建固定线程个数为十个的线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
//new一个Runnable接口的对象
NumberThread number = new NumberThread();
NumberThread1 number1 = new NumberThread1();
//执行线程,最多十个
executorService.execute(number1);
executorService.execute(number);//适合适用于Runnable
//executorService.submit();//适合使用于Callable
//关闭线程池
executorService.shutdown();
}
}
线程的基本控制方法
同步的三种实现方式
java线程的同步问题可以通过三种方式实现:
首先创建四个线程
public class Test01 {
public static void main(String[] args) {
//创建接口实现类实例化对象
Runnable r1 = new TicketRunnableImpl();
//创建线程
Thread t1 = new Thread(r1, "窗口一");
Thread t2 = new Thread(r1, "窗口二");
Thread t3 = new Thread(r1, "窗口三");
Thread t4 = new Thread(r1, "窗口四");
//启动线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
实现方式一:使用synchronized代码块
public class TicketRunnableImpl implements Runnable{
private int ticketNum = 1000;
@Override
public void run() {
while (ticketNum > 0) {
//同步代码块
synchronized (this) {
//判断
if (ticketNum > 0) {
ticketNum--;
System.out.println(Thread.currentThread().getName() + "售出一张票,剩余:" + ticketNum);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
实现方式二:使用对象锁
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TicketRunnableImpl implements Runnable{
private int ticketNum = 1000;
//创建锁对象
Lock lock = new ReentrantLock();
@Override
public void run() {
while (ticketNum > 0) {
//上锁
lock.lock();
//判断
if (ticketNum > 0) {
ticketNum--;
System.out.println(Thread.currentThread().getName() + "售出一张票,剩余:" + ticketNum);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//解锁
lock.unlock();
}
}
}
实现方式三:使用同步方法
public class TicketRunnableImpl implements Runnable{
private int ticketNum = 1000;
@Override
public void run() {
while (ticketNum > 0) {
sellTickets();
}
}
//同步方法
public synchronized void sellTickets() {
//判断
if (ticketNum > 0) {
ticketNum--;
System.out.println(Thread.currentThread().getName() + "售出一张票,剩余:" + ticketNum);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
优先使用顺序:
LOCK-》同步代码块-》同步方法
通信
sleep和wait的异同:
- 相同点:一旦执行方法以后,都会使得当前的进程进入阻塞状态
- 不同点:
- 两个方法声明的位置不同,Thread类中声明sleep,Object类中声明wait。
- 调用的要求不同,sleep可以在任何需要的场景下调用,wait必须使用在同步代码块或者同步方法中
- 关于是否释放同步监视器,如果两个方法都使用在同步代码块或同步方法中,sleep不会释放,wait会释放
线程工具
一、CountDownLatch
1.应用场景
在实际多线程并发开发过程中,我们会碰见很多等待子线程完毕后在继续执行的情况,(如多个子线程下载文件,所有子线程执行完毕后再重命名为文件名)。
2.使用方式
CountDownLatch的构造函数接受一个int类型的参数作为计数器,调用countDwon()方法,计数器减1,await()方法阻塞当前线程,直到计数器变为0;、
补充:
计数器为0的时候,调用awaite()方法不会阻塞主线程;
初始化后,不能修改计数器的值;
可以使用await(long time,TimeUnit unit)等待特定时间后,就不阻塞主线程;
public class Main {
//等待2个子线程执行完毕,计数器为2
static CountDownLatch countDownLatch = new CountDownLatch(2);
public static void main(String[] args) {
System.out.println("start subThread doing...");
//创建并开启2个子线程
SubThread subThread1 = new SubThread();
SubThread subThread2 = new SubThread();
subThread1.start();
subThread2.start();
try {
//阻塞主线程,等待子线程结束
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("subThread are finish...");
}
static class SubThread extends Thread {
@Override
public void run() {
//模拟执行任务
try {
sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//子线程执行完毕,减少计数器
System.out.println(getName() + " done...");
countDownLatch.countDown();
}
}
}
运行结果:当Thread-1、Thread-0两个子线程执行完毕后,在运行main线程后续的逻辑
start subThread doing...
Thread-1 done...
Thread-0 done...
subThread are finish...
二、CyclicBarrier
1.应用场景
如果当你遇见需要让一组线程达到同一个屏障(同步点)时被阻塞,直到最后一个线程达到屏障时,屏障才会打开的情况。
2.使用方式
CycliBarrier默认的构造方法CyclicBarrier(int parties),参数标识屏障拦截的线程个数,每个线程调用await()方法告诉SyclicBarrier我们已经达到屏障了,然后当前线程被阻塞。当所有子线程都达到屏障后,则继续执行子线程的后续逻辑。
补充:
CyclicBarrier还提供了一个更高级的函数CyclicBarrier(int parties,Runnable barrierAction),用于在线程达到屏障时,优先执行barrierAction。
3.实例代码
public class Main {
//拦截2个子线程屏障
static CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
public static void main(String[] args) {
System.out.println("start subThread doing...");
SubThread subThread1 = new SubThread();
SubThread subThread2 = new SubThread();
subThread1.start();
subThread2.start();
}
static class SubThread extends Thread {
@Override
public void run() {
try {
System.out.println(getName() + " doing first things.");
//模拟子线程执行第一个任务
sleep(3000);
System.out.println(getName() + " done first things.");
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
//完成第一个任务,告知达到屏障
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
//所有子线程都完成第一个任务后,继续运行每个子线程的下一个任务
System.out.println(getName() + " doing other things.");
}
}
}
运行结果:当子线程都执行完第一个任务到达屏障后,执行下一个任务
start subThread doing...
Thread-0 doing first things.
Thread-1 doing first things.
Thread-1 done first things.
Thread-0 done first things.
Thread-0 doing other things.
Thread-1 doing other things.
三、Semaphore
1.应用场景
多线程访问公共资源的情况在开发过程中经常遇见,如数据库连接,可能开启几十个线程进行并发读取,但是考虑到数据库连接性能和消耗,我们必须控制10个线程哪个是连接数据库。Semaphore就是用来控制同时访问特定资源的线程数量。
2.使用方式
Semaphore的构造方法Semaphore(int permits),permits标识许可证数量。执行任务前,acquire()方法获取一个许可证;任务执行完成后调用relese()方法归还许可证。没有获得许可证的子线程就阻塞等待。
补充:
tryAcquire():尝试获取许可证;
intavaliablePermits():返回信号量中当前许可证的个数;
intgetQueueLength():返回正在等待获取许可证的线程个数;
booleanhasQueueThreads():是否有线程正在等待许可证;
reducePermits(int reduction):减少reduction个许可证;
getQueuedThreads():返回所有等待获取许可证的线程集合;
3.实例代码
public class Main {
//创建2个许可证
static Semaphore semaphore = new Semaphore(2);
public static void main(String[] args) {
System.out.println("start subThread doing...");
//同时开启4个子线程运行
for (int i = 0; i < 4; i++) {
SubThread subThread = new SubThread();
subThread.start();
}
}
static class SubThread extends Thread {
@Override
public void run() {
try {
//执行任务前获取许可证
semaphore.acquire();
System.out.println(getName() + "doing things.");
sleep(3000);
//执行完任务释放许可证
semaphore.release();
System.out.println(getName() + "finish things.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果:同时只有2个线程运行,当某个线程运行完毕释放许可后,下一个线程才获取许可运行;
start subThread doing...
Thread-0doing things.
Thread-1doing things.
Thread-1finish things.
Thread-2doing things.
Thread-0finish things.
Thread-3doing things.
Thread-2finish things.
Thread-3finish things.
线程组
Java中使用ThreadGroup来表示线程组,对一批线程进行管理,控制一个线程组相当于掌控者属于这个线程组内的所有线程,但线程创建时若没有显示指定线程所属的线程组,则会被分到默认的线程组内
当某个线程加入了某个线程组后,该线程的整个周期都会在该线程组中,线程周期内无法改变该线程所属的线程组
创建Thread类时可以指定当前线程所属的线程组
- Thread(ThreadGroup group,Runnable runnable),该线程属于group线程组,以runnable中的run方法作为线程执行体
- Thread(ThreadGroup group,String name) 以name命名的新线程,属于group线程组
- Thread(ThreadGroup group,Runnable runnable,String name) 以name命名的新线程,该线程属于group线程组,以runnable中的run方法作为线程执行体
Thread提供了一个getThreadGroup()方法,返回当前线程所属的线程组对象,该对象的getName()方法获取线程组的名称
ThreadGroup类的两个构造器
- ThreadGroup(String name) 以指定的name作为名字创建线程组
- ThreadGroup(ThreadGroup parent,String name) 以指定的父线程组,指定的name作为名字创建线程组
注意线程组一旦被创建,线程组的名字不可被更改
ThreadGroup类的几个操作线程组内所有线程的常用方法
- activeCount() 返回线程组内活动的线程数目 int
- isDaemon() 是否为后台线程组 boolean
- setDaemon(boolean bool) 设置线程组为后台线程组----最后一个线程执行结束或者被销毁后,该后台线程组自动销毁
- interrupt() 中断线程组内所有的线程
线程相关类
ThreadLocal类
ThreadLocal类代表一个线程局部变量,通过把数据放在ThreadLocal中就可以让每个线程创建一个该变量的副本,从而避免并发访问的线程安全问题。
ThreadLocal的功能就是为每个使用该变量的线程都提供一个变量值的副本,使每个线程可以独立地改变自己的副本,而不会和其他线程的副本冲突,从线程的角度看,每个线程都完全拥有该变量一样。
ThreadLocal并不能替代同步机制,两者面向的问题领域不同。同步机制是为了同步多个线程对相同资源的并发访问,是多个线程之间进行通信的有效方式;而ThreadLocal是为了隔离多个线程的数据共享,从根本上避免多个线程之间对共享资源的竞争。
如果多个线程之间需要共享资源,以达到线程之间的通信功能就使用同步机制;如果仅仅需要隔离多个线程之间的共享冲突,则可以使用ThreadLocal。
Collection包装线程不安全的集合
ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等都是线程不安全的,即当多个并发线程都向这些集合中存、取元素时,就可能破坏这些集合的数据完整性。
如果程序中有多个线程可能访问以上集合,那么就可能用Collections提供的静态方法把这些集合包装成线程安全集合。
线程安全的集合类
1、Concurrent开头集合
以Concurrent开头的集合类,如ConcurrentHashMap、ConcurrentSkipList、ConcurrentSkipListSet、ConcurrentLinkedQueue和ConcurrentLinkedDeque。
2、CopyOnWrite开头集合
以CopyOnWrite开头的集合类,如CopyOnWriteArrayList、CopyOnWriteArraySet。