最近刚学完多线程,趁着自己还没忘记,现总结总结,大家凑合着看看吧
1.多线程的实现方法
1.1并行与并发
并行:多个对象同一时刻执行多个任务
并发:单个对象在同一时间段交替执行多个任务
1.2进程和线程
进程:指正在运行的一个程序(cpu分配资源的基本单位)
线程:正在运行的程序中的一个执行任务(执行单元)(进程中分配资源的基本单位)
1.3多线程的三种实现方法
1.3.1继承Thread类
一个类通过继承Thread类,重写父类的run()方法,来实现多线程,通过这种方法来实现多线程,直接创建对象调用start()方法来开始线程;
start()方法实际上是开辟新的栈内存,在新的栈内存中执行线程任务
所以要想开始新的线程,必须调用start()方法
1.3.2实现Runnable接口
一个类可以通过实现Runnable接口来实现多线程,重写接口中的run方法,然后将实现了接口的类的对象作为参数传递给Thread对象,再通过调用start()方法来开启线程:new Thread(Runnable对象)
1.3.3实现Callable接口
一个类可以通过实现Callable接口来实现多线程,重写接口的call方法,然后创建类的对象作为参数传递给FutureTask对象,然后将FutureTask对象作为参数传递给Thread对象:
FutureTask ft = new FutrueTask(Call对象);
Thread t = new Thread(ft);
t.start();
call方法是有一个返回值的,Callable是一个泛型接口,泛型的类型必须和call方法的返回值类型相同
在线程运行结束后,可以通过FutureTask对象调用get方法来获取返回值
1.3.4三种方式的比较
继承Thread相比较起来编写代码少,但是该类无法去继承其他类,降低了可扩展性
其他两种实现接口的方式提高了可扩展性,在实现多线程时还可以去继承其他类(以后用实现接口较多)
2.多线程的成员方法
3.1设置获取名字
String getName();//获取线程的名字
Thread(String name);//通过构造方法给线程定义一个名字
setName(String name);//设置线程的名字
3.2获取当前线程对象
public static Thread currentThread();//获取当前调用方法的线程对象
3.3睡眠方法
public static void sleep(long time);//设置当前线程休眠time时间
3.4线程的优先级
声明:线程的优先级的高低只是说明其抢占CPU时间片的概率的大小,并不能保证在CPU空闲时一定能抢到执行权
int getPriority();//获取当前线程的优先级
void setPriority(int priority)//给线程设置优先级
注:优先级默认为5,范围为MIN_PRIORITY(1)-MAX_PRIORITY(10)
3.5守护线程
void setDaemon(boolean false);//设置一个线程为守护线程
守护线程时为了给其他线程做辅助工作,当普通线程结束时不管守护线程是否结束,它都会随之结束
3.多线程的安全问题
3.1产生安全问题原因
当多个线程共享资源时,并且多个线程都对资源数据进行增删改操作时,会出现安全问题。
3.2解决方法
3.2.1同步代码块
同步代码块的格式:
synchronized(锁对象){//多线程对共享资源操作的代码 }
注意:①锁对象可以是任意对象
②多线程必须用同一把锁
同步代码块解决多线程安全问题的原理:使一个线程在对共享数据进行操作时,其他线程无法对其进行操作
3.2.2同步方法
同步方法的格式:
public [static] synchronized 返回值类型 方法名(){}
static同步方法 锁对象是 this
非static同步方法 锁对象是 类名.class
3.2.3创建锁对象
可以自己实例化一个锁对象:Lock lock = new ReenTrantLock
在需要加锁的代码位置lock();需要解开锁的位置unlock();
相关代码:
这里是同步代码块写了一个经典的卖票的线程安全问题,同步方法不做演示
public class MySynchronizedTest1 {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket);
Thread t2 = new Thread(ticket);
Thread t3 = new Thread(ticket);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Ticket implements Runnable{
//定义票的数量
private int ticket = 100;
private static Object obj = new Object();
@Override
public void run() {
while(true){
//设置同步代码块,将操作共享数据的代码放到synchronized(Object obj){}的大括号中
synchronized (obj){
//就是当前的线程对象.
if(ticket <= 0){
break;//说明票已经卖完了
}else{
//卖每一张票需要时间,所以这里让它睡眠一下再继续卖下一张
try {
Thread.sleep(150);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
}
}
}
}
}
3.3死锁问题
死锁:由于锁的嵌套导致线程A等待线程B的锁,线程B等待线程A的锁,所以导致程序无法正常执行
代码演示:
//死锁
public class MySynchronizedTest3 {
public static void main(String[] args) {
Object objA = new Object();
Object objB = new Object();
new Thread(()->{
while(true){
synchronized (objA){
synchronized (objB){
System.out.println("我在走路");
}
}
}
}).start();
new Thread(()->{
while(true){
synchronized (objB){
synchronized (objA){
System.out.println("你在走路");
}
}
}
}).start();
}
}
3.4生产者-消费者
这里举例说明可以更好的理解:
①顾客来到餐厅发现没有吃的,就会等待wait()
②顾客来到餐厅发现有吃的,那么就会吃掉吃的,然后通知厨师去做notify()
③厨师发现现在有吃的,那就等待wait()
④厨师发现没有吃的,那么就会做食物,然后通知顾客notify()
生产者-消费者主要解决了让多个线程按照一定的次序来协作完成某个功能
代码实现主要使用三个方法:wait() notify() notifyAll();
注意:这三个方法的调用者都是锁对象
附上代码如下:
//桌子类,存放共享资源
public class Desk {
//定义一个flag表示桌子上是否有食物
private boolean flag;
//定义食物数量
private int FoodCount;
//定义共用的锁
private final Object Lock = new Object();
public Desk() {
this(false,10);
}
public Desk(boolean flag, int foodCount) {
this.flag = flag;
FoodCount = foodCount;
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public int getFoodCount() {
return FoodCount;
}
public void setFoodCount(int foodCount) {
FoodCount = foodCount;
}
public Object getLock() {
return Lock;
}
@Override
public String toString() {
return "Desk{" +
"flag=" + flag +
", FoodCount=" + FoodCount +
", Lock=" + Lock +
'}';
}
}
//消费者类(顾客)
public class Foodie extends Thread{
private Desk desk;
public Foodie(Desk desk) {
this.desk = desk;
}
@Override
public void run() {
while(true){
synchronized (desk.getLock()){
if(desk.getFoodCount() == 0){
break;
}else{
if(desk.isFlag()){//代表桌子上有食物
System.out.println("吃货正在吃倒数第"+desk.getFoodCount()+"个食物");
desk.setFlag( false);//吃完之后将flag变为flase表示桌子上没有食物了
//Desk.FoodCount--;//食物数量减一
desk.setFoodCount(desk.getFoodCount()-1);
desk.getLock().notifyAll();//唤醒等待的线程
}else{
try {
desk.getLock().wait();//如果为false,说明没有食物,那么进入等待状态,释放掉锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
//生产者类(厨师)
public class Cooker extends Thread{
private Desk desk;
public Cooker(Desk desk) {
this.desk = desk;
}
@Override
public void run() {
while(true){
synchronized (desk.getLock()){
if(desk.getFoodCount() == 0){
break;
}else{
if(!desk.isFlag()){
System.out.println("厨师正在做倒数第"+desk.getFoodCount()+"个食物,还剩"+(desk.getFoodCount()-1)+"个食物");
desk.setFlag(true);
desk.getLock().notifyAll();
}else{
try {
desk.getLock().wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
//测试类
public class Test {
public static void main(String[] args) {
Desk desk = new Desk();
Foodie f = new Foodie(desk);
Cooker c = new Cooker(desk);
f.start();
c.start();
}
}
3.5阻塞队列
3.5.1阻塞队列的使用
阻塞出现的原因:程序去完成一个功能时,由于某种原因,现在无法完成,程序会停住(后面的代码就不能再执行了),直到该功能完成为止。
阻塞队列结合生产者-消费者使用可以大大提高代码效率,并且不需要加锁,因为阻塞队列的底层就是 锁+wait+notify
3.5.2阻塞队列的创建和方法
BlockingQueue:
ArrayBlockingQueue(int capacity)
LinkedBlockingQueue();
方法:
往阻塞队列中放元素:put
从阻塞队列中取元素:take
3.5.3分析
在消费者-生产者机制中使用阻塞队列,相当于给生产者提供了一个可以存放多个产品的队列,它生产了产品就放到阻塞队列中,不必等待消费者消费了产品之后再唤醒它再去生产下一个产品;而消费者也同理,它可以直接从阻塞队列中取产品,不用等生产者一个一个生产。
附上代码如下:
import java.util.concurrent.ArrayBlockingQueue;
//生产者类
public class Cooker extends Thread{
private ArrayBlockingQueue<String> arrayBlockingQueue;
public Cooker(ArrayBlockingQueue<String> arrayBlockingQueue) {
this.arrayBlockingQueue = arrayBlockingQueue;
}
@Override
public void run() {
while (true){
try {
arrayBlockingQueue.put("汉堡包");
System.out.println("往阻塞队列中放一个汉堡包");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//消费者类
import java.util.concurrent.ArrayBlockingQueue;
public class Foodie extends Thread{
private ArrayBlockingQueue<String> arrayBlockingQueue;
public Foodie(ArrayBlockingQueue<String> arrayBlockingQueue) {
this.arrayBlockingQueue = arrayBlockingQueue;
}
@Override
public void run() {
while (true){
try {
String food = arrayBlockingQueue.take();
System.out.println("从阻塞队列拿到了一个"+food);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//测试类
import java.util.concurrent.ArrayBlockingQueue;
public class Test {
public static void main(String[] args) {
ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(1);
Cooker c = new Cooker(arrayBlockingQueue);
Foodie f = new Foodie(arrayBlockingQueue);
c.start();
f.start();
}
}
3.6线程的状态
线程的状态有6种,分别是:
new:Thread对象new好之后的状态
runnable:Thread对象调用.start方法后
blocked:阻塞
waiting:等待
timed_waiting:计时等待
terminated:死亡
3.7线程池
3.7.1概念
线程池指存放线程的容器,大大提高了效率,不需要每需要执行一个任务就创建一个线程,执行完成后销毁线程;线程池的存在可以使线程任务结束后回到线程池,下次任务执行再直接使用
3.7.2Executors默认线程池
Executors:
static ExecutorService newCachedThreadPool();
这是一个静态方法,可以通过Executors这个类直接调用,返回一个ExecutorService对象
ExecutorService提供了这些方法:
submit(Runable r);//接收一个Runable接口的实现类,可以使用Lambda表达式
submit(Callable c);//接收一个Callable接口的实现类,可以使用Lambda表达式
shutdown();//摧毁线程池
3.7.3Executors创建指定上限的线程池
要想创建指定线程池容量的方法:
ExecutorService newFixedThreadPool(int nTheads);
ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
ScheduledExecutorService newSingleThreadScheduledExecutor()
ExecutorService newSingleThreadExecutor()
3.7.4自己直接创建线程池
上面的创建都是先创建Service,我们也可以直接创建线程池对象:
public ThreadPoolExecutor(
int corePoolSize,//核心线程数量 2
int maximumPoolSize,//最大线程数量 5
long keepAliveTime,//空闲线程最大的空闲时间
TimeUnit unit,//空闲时间单位
BlockingQueue<Runnable> workQueue, //阻塞队列,存放等待执行的任务
ThreadFactory threadFactory,//造线程的工厂
RejectedExecutionHandler handler//任务超过最大线程数+阻塞队列容量,拒绝的策略)
3.7.4.1拒绝策略
当任务超过最大线程数+阻塞队列容量会被拒绝,默认的拒绝策略是丢弃任务,并抛出异常
3.7.4.2非默认任务拒绝策略
ThreadPoolExecutor.DiscardPolicy:
*丢弃任务;
ThreadPoolExecutor.DiscardOldestPolicy:
*丢弃任务队列中等待时间最长的任务,再把最新的任务添加到队列中
ThreadPoolExecutor.CallerRunsPolicy:
*谁提交的这个任务,谁来执行这个任务;
附上线程池相关代码:
public class MyThreadPoolDemo1 {
public static void main(String[] args) {
//创建线程池对象
ExecutorService executorService = Executors.newCachedThreadPool();
//使用Lambda表达式来创建函数式接口的对象
executorService.submit(()->{
System.out.println(Thread.currentThread().getName()+"正在运行");
});
//使用Lambda表达式来创建函数式接口的对象
executorService.submit(()->{
System.out.println(Thread.currentThread().getName()+"正在运行");
});
executorService.submit(()->{
System.out.println(Thread.currentThread().getName()+"正在运行");
});
executorService.shutdown();
}
}
上边是通过Executors默认创建线程池,下边是自己new的线程池:
public class MyThreadPoolDemo3 {
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3,//核心线程数量
5,//总线程数量
2,//临时线程的空闲时间
TimeUnit.SECONDS,//空闲时间单位
new ArrayBlockingQueue<>(10),//阻塞队列
Executors.defaultThreadFactory(),//创建线程的线程工厂
new ThreadPoolExecutor.AbortPolicy());//当等待的线程超过总量+阻塞队列时的拒绝策略
pool.submit(()->{
System.out.println(Thread.currentThread().getName()+"RUNNING....");
});
pool.shutdown();
}
}
3.8volatile-问题
volatile关键字主要解决了内存不可见问题,就是当多个线程同时操作共享数据时,如果其中一个修改了共享数据,另外的线程无法及时获取到新的数据。
使用volatile关键字修饰变量后,那么有关该变量操作的代码,就不会被机器指令替换
内存不可见问题:jvm在执行java代码时,针对反复执行的代码,就会把这些代码标记为热点代码,这些热点代码在执行指定量次(10000),jvm会使用jit及时编译,把之前这些class指令替换为机器指令,反复执行的时候就再也不会读取新的数据了
也可以使用synchronized解决内存不可见问题,因为synchronized中的代码会强制查看共享内存中的数据;
3.9原子性
原子性指不可分割,举例:count++
count++虽然看起来是一行代码,但是执行的时候对应3个指令:
1.读取共享内存数据到变量副本;
2.对变量副本中的数据进行自增1;
3.把变量副本中的数据写入到共享内存;
所以当多个线程执行时,count++可能造成线程安全问题,
可以使用synchronized解决原子性导致的线程安全问题,将count++变成不可分的,但是synchronized是重量级锁,性能较低,也可以使用volatile+cas算法解决线程安全问题。
3.9.1AtomicInteger
jdk在并发包中提供了大量的Atomicxxx类,专门用于在多线程环境下编程,AtomicInteger是比较常用的一种:
AtomicIneger();
int get(); //获取值
int getAndIncrement();//以原子方式将当前值加1,这里返回的是自增前的值
int incrementAndGet();// 以原子方式将当前值加1,这里返回的是自增后的值
int addAndGet(int delta);//以原子方式将参数与对象中的值相加,并返回结果
int getAndSet(int delta);//以原子方式设置为newValue的值,并返回旧值。
3.9.2AtomicInteger-内存解析
volatile关键字加CAS算法可以解决内存不可见问题
CAS算法原理:在对共享数据进行操作时,把原来的旧值记录下来,在自己的栈内对数据进行操作之后要写入内存之前,比较现在内存中的值跟原来的旧值一样,那么说明没有其他线程对共享数据进行操作,那么对其进行修改(将修改的值写入内存);如果旧值不一样了,那么说明有其他线程对共享数据进行过其他操作了,那么需要再次获取内存的新值,再次进行操作,这个重新获取就是自旋。
伪代码:
while(true){
①读取内存值,赋值给旧值;
②给旧值加1,得到新值;
③if(旧值==内存值){
内存值=新值;
break;
}
}
3.10并发工具类
3.10.1HashTable
HashTable是线程安全的,因为它的底层方法都加了synchronized关键字,在操作共享数据时,将整个hash表都锁
3.10.2并发工具类-ConcurrentHashMap
ConcurrentHashMap继承了HashMap,所以父类的方法都是可以使用的
ConcurrentHashMap在JDK1.7中,有一个长度为16的不可变的数组,在每个数组位置上有一个可扩容小数组,该位置存储的数据超过小数组的加载因子,则自动扩容为原长度的2倍
ConcurrentHashMap在JDK1.8中,采用了 CAS + synchronized 来保证并发安全性
put方法:
1.计算key的hash值
2.如果当前table还没有初始化先调用initTable方法将tab进行初始化
3.tab中索引为i的位置的元素为null,则直接使用CAS将值插入即可
4.当前正在扩容
6.若当前为红黑树,将新的键值对插入到红黑树中
7.插入完键值对后再根据实际大小看是否需要转换成红黑树
8.对当前容量大小进行检查,如果超过了临界值(实际大小*加载因子)就需要扩容
3.10.3ConcurrentHashMap的高性能
在操作数据时只锁住索引处的链表,不像HashTable锁住一整张表
3.10.4并发工具类-CountDownLatch
如果一个线程需要等待其他多个线程执行完毕以后才能执行,可以使用CountDownLatch
public CountDownLatch(int count)://让当前线程等待,计数器为0时唤醒
public void await():
public void countDown():
附上完整代码:
//这里写了一个妈妈要等待孩子都吃完饺子才收拾碗筷的案例
import java.util.concurrent.CountDownLatch;
//孩子线程
public class ChildrenRunnable implements Runnable{
private CountDownLatch countDownLatch;
private int number;
public ChildrenRunnable(CountDownLatch countDownLatch, int number) {
this.countDownLatch = countDownLatch;
this.number = number;
}
@Override
public void run() {
for (int i = 1; i <= number; i++) {
System.out.println(Thread.currentThread().getName()+"正在吃第"+i+"个饺子");
}
countDownLatch.countDown();
}
}
import java.util.concurrent.CountDownLatch;
//妈妈线程
public class MotherRunnable implements Runnable{
private CountDownLatch countDownLatch;
public MotherRunnable(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("妈妈去收拾碗筷了");
}
}
import java.util.concurrent.CountDownLatch;
public class MyCountDownLatchDemo {
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(3);//这里表示有三个线程要等待
MotherRunnable mr = new MotherRunnable(countDownLatch);
new Thread(mr).start();
new Thread(new ChildrenRunnable(countDownLatch,7),"小黑").start();
new Thread(new ChildrenRunnable(countDownLatch,9),"小红花").start();
new Thread(new ChildrenRunnable(countDownLatch,11),"小狗").start();
}
}
3.10.5并发工具类-Semaphore
限制资源的执行量时 使用Semaphore
public Semaphore(int permits)
public void acquire()//获取信号量----阻塞方法
public void release()//释放信号量
完整代码:
//这里模拟一个路口同时只允许三辆车通过,必须拿到通行证才能通过,通过后归还通行证,然后别的车才能拿到通行证
public class MySemaphoreRunnable implements Runnable{
//获取管理员对象
private Semaphore semaphore = new Semaphore(3);
@Override
public void run() {
try {
semaphore.acquire();//相当于获取通行证
System.out.println("拿到通行证,可以通行");
Thread.sleep(2000);
System.out.println("归还通行证");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class MySemaphoreDemo {
public static void main(String[] args) {
MySemaphoreRunnable msr = new MySemaphoreRunnable();
for (int i = 0; i < 100; i++) {
new Thread(msr).start();
}
}
}