多线程(看这篇就够了!最全)

基本概念

进程是电脑程序运行的最小单位,而线程则是进程的最小执行单位
多线程呢就是指一个进程运行时,多个线程并行交替执行的过程。可以提高代码的执行效离

实现方式

1.继承thread类
特点:因为是通过继承的方式,所以该类就不能继承其他的类了,有局限性
使用:直接使用该类调用start方法
2.实现Runnable接口
特点:通过实现接口的方式避免了第一种方式的单继承的局限性问题
使用:new Thread(实现了Runnable的类对象).start()
3.实现Callnable接口
Runnable和Callable的区别
Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
Call方法可以抛出异常,run方法不可以。
运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

解决多线程安全

1.使用synchronized

synchronized的三种应用方式

修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁

修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁

修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
在这里插入图片描述

在这里插入图片描述

public class a {
    
    
    //方法1
    public void display1(Thread t) {
    
    
        synchronized (this){
    
    
            System.out.println(t.getName());
            for (int i = 0; i < 100; i++) {
    
    
                System.out.println(i);
            }
        }
    }

    //方法2
    public synchronized void display2(Thread t) {
    
    
            System.out.println(t.getName());
            for (int i = 0; i < 100; i++) {
    
    
                System.out.println(i);
        }
    }
    public static void main(String[] args) {
    
    
        a a = new a();
        Thread thread1 = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                a.display2(Thread.currentThread());
            }
        },"a1");
        Thread thread2 = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                a.display2(Thread.currentThread());
            }
        },"a2");
         thread1.start();
         thread2.start();
    }
}

讲解一下:
synchronized 是java的一个关键字,主要是用来解决线程安全问题的,被synchronized 修饰的方法或者代码块,在任意时刻都只能有一个线程去执行;在老版本中synchronized 是一个重量级锁,比较消耗资源;但在jdk1.6之后,加入了偏向锁,自旋锁,适应性自旋锁,轻量级锁,锁粗化等,大大的提升了synchronized 的性能,也不再是我们所说的重量级锁。
偏向锁它是针对于一个线程去设计的,这个锁会偏向于第一个获得它的线程。我们知道,对象的结构包括,对象头,实例数据,填充数据,在对象头中,markword中有一个标记位用来存储是否偏向锁的标记,0表示未偏向,1表示已偏向。
1.当执行到临界区(所谓临界区,就是只允许一个线程进去执行操作的区域,即同步代码块。CAS是一个原子性操作)时,首先会将该线程id通过cas操作存储到markword中,并且标记为已偏向。
2.当之后的线程再执行时,首先判断线程id是否与markword中一致,如果一致,那么不再去进行释放锁和获取锁的操作,会继续正常执行
3.如果线程id不一致,那么会去判断一下该对象是否偏向,如果没有则通过cas再设置一次,如果已经偏向,那么说明偏向的并不是自己,那么就存在(竞争),根据情况可能是重新偏向,也可能是偏向撤销,但是大部分情况下就会升级为轻量级锁。
综上所述,偏向锁是针对与一个线程设计的,当有两个或两个以上的线程竞争锁的时候,那么偏向锁就会失效,锁膨胀就会升级为轻量级锁
(为什么要这样做呢?因为经验表明,其实大部分情况下,都会是同一个线程进入同一块同步代码块的。这也是为什么会有偏向锁出现的原因。)
轻量级锁
轻量级锁主要有两种:
自旋锁
自适应自旋锁
自旋锁:当线程去竞争锁的时候不会去阻塞,而是去循环不断地尝试获取锁,但也有个问题,假如线程的执行时间长,每次都获取不到,那么会一直的循环等待。这时我们可以设置一下他的自旋次数,默认是十次,当超过十次尝试还未获取时,那么就会升级为重量级锁
自适应自旋锁:就是会动态的改变自旋次数,比如刚获取过锁的线程再去自旋,那么就会增加它的自旋次数,因为会认定它很有可能再次获取到锁。当一个从未获取到锁的线程再去自旋,也有可能不去自旋直接升级成重量级锁。
重量级锁
重量级锁为什么消耗资源呢?因为当有线程尝试获取重量级锁的时候,那么会将它阻塞,当再次唤醒的时候都需要借助操作系统去完成,也就是要从用户态转为内核态,而转换状态也是非常消耗资源的,可能比代码执行的时间还要长

2.使用lock锁

用法

lock实现ReentrantLock

常用方法及用处:
lock() 获取锁,获取不到会一直等待
trylock() 返回值是boolean类型,拿到锁就返回true,没有拿到就返回false,带时间限制的tryLock(),拿不到lock,就等一段时间,超时返回false
unlock() 释放锁

lock实现ReetrantReadWriteLock

读写锁的特性
1.获取锁顺序问题
非公平模式:读锁和写锁的获取的顺序是不确定的,可能会延缓一个或多个读或写线程,但是会比公平锁有更高的吞吐量
公平模式:线程将会以队列的顺序获取锁,等待锁时间越长的先获取
2.可重入:就是说一个线程在获取某个锁后,还可以继续获取该锁。synchronized内置锁就是可重入的,如果不可重入,那么比如一个类的两个方法都被synchronized所修饰,那么方法A就不能够调方法B了。
3.锁降级
首先要知道
锁升级:从读锁变成写锁
锁降级:从写锁变成读锁;
ReetrantReadWriteLock是不支持锁升级的,会产生死锁问题
4.读锁跟写锁是互斥的

举例:


public class a {
    
    
    Lock lock = new ReentrantLock(); //用于方法1
    ReentrantReadWriteLock   locks = new ReentrantReadWriteLock  (); //用于方法2(读写锁)
    
    //方法1   一般要写成try catch块的形式,把lock.unclock()方法放到finally里面
    public void display1(Thread t) {
    
    
        lock.lock();
            System.out.println(t.getName()+":获取到了锁");
            for (int i = 0; i < 100; i++) {
    
    
                System.out.println(i);
            }
        lock.unlock();
    }

    //方法2
    public void display2(Thread t) {
    
    
        locks.writeLock().lock();
//        locks.writeLock().lock();
        try{
    
    
            System.out.println(t.getName()+":获取到了锁");
            for (int i = 0; i < 100; i++) {
    
    
                System.out.println(i);
            }
        }catch (Exception e){
    
    
           e.printStackTrace();
        }finally {
    
    
            locks.writeLock().unlock();
//            locks.writeLock().unlock();
        }
    }


    //方法3   锁降级
    public void display4(Thread t) {
    
    
        locks.writeLock().lock();
        locks.readLock().lock();
        try{
    
    
            System.out.println(t.getName()+":获取到了锁");
            for (int i = 0; i < 100; i++) {
    
    
                System.out.println(i);
            }
        }catch (Exception e){
    
    
           e.printStackTrace();
        }finally {
    
    
            locks.readLock().unlock();
            locks.writeLock().unlock();
        }
    }

    //方法4   锁升级  (会产生死锁问题)
    public void display4(Thread t) {
    
    
    	locks.readLock().lock();
        locks.writeLock().lock();
        try{
    
    
            System.out.println(t.getName()+":获取到了锁");
            for (int i = 0; i < 100; i++) {
    
    
                System.out.println(i);
            }
        }catch (Exception e){
    
    
           e.printStackTrace();
        }finally {
    
    
        	locks.writeLock().unlock();
            locks.readLock().unlock();
        }
    }

    public static void main(String[] args) {
    
    
        a a = new a();
        Thread thread1 = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
            //测试其他的,这里改display1即可
                a.display1(Thread.currentThread());
            }
        },"a1");
        Thread thread2 = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
            //测试其他的,这里改display1即可
                a.display1(Thread.currentThread());
            }
        },"a2");
         thread1.start();
         thread2.start();
    }
}

注意
使用lock锁时,lock必须定义到类里面,这样线程调用的时候拿到的是同一个lock对象,这样才能锁住

对比synchronized使用,测试读操作效率

public class ReadAndWriteLockTest {
    
    
    public synchronized static void get(Thread thread) {
    
    
        System.out.println("start time:" + System.currentTimeMillis());
        for (int i = 0; i < 5; i++) {
    
    
            try {
    
    
                Thread.sleep(20);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println(thread.getName() + ":正在进行读操作……");
        }
        System.out.println(thread.getName() + ":读操作完毕!");
        System.out.println("end time:" + System.currentTimeMillis());
    }

    public static void main(String[] args) {
    
    
        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                get(Thread.currentThread());
            }
        }).start();

        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                get(Thread.currentThread());
            }
        }).start();
    }
}

在这里插入图片描述

从运行结果可以看出,两个线程的读操作是顺序执行的,整个过程大概耗时200ms。
public class ReadAndWriteLockTest {
    
    
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public static void get(Thread thread) {
    
    
        lock.readLock().lock();
        System.out.println("start time:" + System.currentTimeMillis());
        for (int i = 0; i < 5; i++) {
    
    
            try {
    
    
                Thread.sleep(20);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println(thread.getName() + ":正在进行读操作……");
        }
        System.out.println(thread.getName() + ":读操作完毕!");
        System.out.println("end time:" + System.currentTimeMillis());
        lock.readLock().unlock();
    }

    public static void main(String[] args) {
    
    
        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                get(Thread.currentThread());
            }
        }).start();

        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                get(Thread.currentThread());
            }
        }).start();
    }
}

在这里插入图片描述

从运行结果可以看出,两个线程的读操作是同时执行的,整个过程大概耗时100ms。通过两次实验的对比,我们可以看出来,ReetrantReadWriteLock的效率明显高于Synchronized关键字

判断读锁跟写锁互斥

public class ReadAndWriteLockTest {
    
    

    public static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public static void main(String[] args) {
    
    
        //同时读、写
        ExecutorService service = Executors.newCachedThreadPool();
        service.execute(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                readFile(Thread.currentThread());
            }
        });
        service.execute(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                writeFile(Thread.currentThread());
            }
        });
    }

    // 读操作
    public static void readFile(Thread thread) {
    
    
        lock.readLock().lock();
        boolean readLock = lock.isWriteLocked();
        if (!readLock) {
    
    
            System.out.println("当前为读锁!");
        }
        try {
    
    
            for (int i = 0; i < 5; i++) {
    
    
                try {
    
    
                    Thread.sleep(20);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                System.out.println(thread.getName() + ":正在进行读操作……");
            }
            System.out.println(thread.getName() + ":读操作完毕!");
        } finally {
    
    
            System.out.println("释放读锁!");
            lock.readLock().unlock();
        }
    }

    // 写操作
    public static void writeFile(Thread thread) {
    
    
        lock.writeLock().lock();
        boolean writeLock = lock.isWriteLocked();
        if (writeLock) {
    
    
            System.out.println("当前为写锁!");
        }
        try {
    
    
            for (int i = 0; i < 5; i++) {
    
    
                try {
    
    
                    Thread.sleep(20);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                System.out.println(thread.getName() + ":正在进行写操作……");
            }
            System.out.println(thread.getName() + ":写操作完毕!");
        } finally {
    
    
            System.out.println("释放写锁!");
            lock.writeLock().unlock();
        }
    }
}

在这里插入图片描述
总结
1.Java并发库中ReetrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性
2.ReetrantReadWriteLock读写锁的效率明显高于synchronized关键字
3.ReetrantReadWriteLock读写锁的实现中,读锁使用共享模式;写锁使用独占模式,换句话说,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的
4.ReetrantReadWriteLock读写锁的实现中,需要注意的,当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁


Lock实现Condition接口

经典用例:可以实现生产者跟消费者的实例

lock可以使用Condition,Condition是一个多线程间协调通信的工具类,要与一个lock绑定使用

3.volatile关键字的使用

①volatile变量是Java语言提供了一种稍弱的同步机制
②volatile能够保证字段的可见性
②volatile能够禁止指令重排
(所谓指令重排是指,比如创建一个对象,首先会分配内存,然后初始化,然后将引用指向该内存地址。在单线程下,执行顺序不会影响什么,但是在多线程情况下,比如按1 3 2的顺序去执行了,那么就会出问题,所以保证其不重排顺序很关键)
原理:volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值
究竟能否保证线程安全?
答案是在一定条件下能,1.写入值时不依赖于当前值,不包含其他变量
经典用法
做状态标记量

 volatile boolean flag ;
    ……
    while!flag ){
    
    
        ……
    }
    public void setFlag() {
    
    
       flag = true;
    }

synchronized 跟lock的区别

synchronized是java的一个关键字,是内置的语言实现的,是用来解决多线程安全问题的,他不能去获取到锁状态,当遇到异常的时候会自动的释放掉锁,synchronized使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度,Synchronized存在明显的一个性能问题就是读与读之间互斥

lock是一个接口,
lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断;

Lock可以通过trylock来知道有没有获取锁,而synchronized不能;

Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

控制线程执行顺序

1.使用join去控制

t1 ,t3,t2的顺序执行

t1.start();
t1.join();
t3.start();
t3.join();
t2.start();
2.使用Excutors.newSingleThreadExecutor()

利用并发包里的Excutors的newSingleThreadExecutor产生一个单线程的线程池,而这个线程池的底层原理就是一个先进先出(FIFO)的队列。代码中executor.submit依次添加了123线程,按照FIFO的特性,执行顺序也就是123的执行结果,从而保证了执行顺序。

public class Testex implements Runnable {
    
    
    @Override
    public void run() {
    
    
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName()+"进来了");
        for (int i = 0; i < 20; i++) {
    
    
            System.out.println(i);
        }
    }
    @Test
    public void test1() throws InterruptedException {
    
    
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Thread t1 = new Thread(new Testex());
        Thread t2 = new Thread(new Testex());
        Thread t3 = new Thread(new Testex());
        executor.submit(t2);
        executor.submit(t1);
        executor.submit(t3);
        executor.shutdown();
    }
}
3.使用countDownLatch
  • countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。
  • 是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException {
    
     };   
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException {
    
     };  
//将count值减1
public void countDown() {
    
     };  

测试代码

public class TestCount{
    
    
	//使用 await()
    @Test
    public void test01() throws InterruptedException {
    
    
        //保证线程1在线程2前执行
    	//设置计数为1,当线程1执行完后就会执行线程2.
    	//试着设置成2,就会一直阻塞,因为线程1执行完后,count值为1,而await方法会一直等待count值为0时才会去执行
        CountDownLatch countDownLatch = new CountDownLatch(1);
          // CountDownLatch countDownLatch = new CountDownLatch(2);
        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                Thread thread = Thread.currentThread();
                System.out.println(thread.getName()+"   进来了");
                for (int i = 0; i < 10; i++) {
    
    
                    System.out.println(thread.getName()+"   "+i);
                }
                countDownLatch.countDown();
            }
        }).start();
        //一直等待count为0后再去继续执行
        countDownLatch.await();
            new Thread(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    Thread thread = Thread.currentThread();
                    System.out.println(thread.getName()+"   进来了");
                    for (int i = 0; i < 10; i++) {
    
    
                        System.out.println(thread.getName()+"   "+i);
                    }
                }
            }).start();
        }


    @Test
    public void test02() throws InterruptedException {
    
    
        CountDownLatch countDownLatch = new CountDownLatch(2);
        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                Thread thread = Thread.currentThread();
                System.out.println(thread.getName()+"   进来了");
                for (int i = 0; i < 10; i++) {
    
    
                    System.out.println(thread.getName()+"   "+i);
                }
                countDownLatch.countDown();
            }
        }).start();
        //3秒后,如果count还不是0 那就执行
        countDownLatch.await(3, TimeUnit.SECONDS);
        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                Thread thread = Thread.currentThread();
                System.out.println(thread.getName()+"   进来了");
                for (int i = 0; i < 10; i++) {
    
    
                    System.out.println(thread.getName()+"   "+i);
                }
                countDownLatch.countDown();
            }
        }).start();
    }
}

思考

为什么在分布式情况下synchronized和lock会失效?
答:因为他们的前提条件必须是同一进程内,而分布式服务里面,多个服务属于不同进程,所以用普通的同步锁会失效,这里就要用到分布式锁去处理

分布式锁

待写。。。

线程池

使用Executors工具类去创建

我们之前使用线程的时候都是使用new Thread来进行线程的创建,但是这样会有一些问题。如:

a. 每次new Thread新建对象性能差。
b. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
c. 缺乏更多功能,如定时执行、定期执行、线程中断。
相比new Thread,Java提供的四种线程池的好处在于:
a. 重用存在的线程,减少对象创建、消亡的开销,性能佳。
b. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
c. 提供定时执行、定期执行、单线程、并发数控制等功能。

Java通过Executors提供四种线程池,分别为:
newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool :创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool :创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor :创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

参考代码:

    @Test
    public  void test1() {
    
    
        //定长
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
    
    
            executorService.execute(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    try{
    
    
                        System.out.println(Thread.currentThread().getName()+"sleep之前");
                        Thread.sleep(1000);
                        System.out.println(Thread.currentThread().getName()+"sleep之后");
                    }catch (Exception e){
    
    
                        e.printStackTrace();
                    }
                }
            });
        }
    }
    @Test
    public  void test2() {
    
    
        //定长
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
    
    
            executorService.execute(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    try{
    
    
                        Thread.sleep(3000);
                        System.out.println(Thread.currentThread().getName());
                    }catch (Exception e){
    
    
                        e.printStackTrace();
                    }
                }
            });
        }
    }

弊端
1)newFixedThreadPool和newSingleThreadExecutor:

主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。

2)newCachedThreadPool和newScheduledThreadPool:

主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
因此推荐使用ThreadPoolExecutor去创建线程
先看看构造方法

 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

参数:
corePoolSize:指定了线程池中的线程数量,它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中去;

maximumPoolSize:指定了线程池中的最大线程数量,这个参数会根据你使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量;

keepAliveTime:当线程池中空闲线程数量超过corePoolSize时,多余的线程会在多长时间内被销毁;

unit:keepAliveTime的单位

workQueue:任务队列,被添加到线程池中,但尚未被执行的任务;它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列几种;

threadFactory:线程工厂,用于创建线程,一般用默认即可;

handler:拒绝策略;当任务太多来不及处理时,如何拒绝任务;

猜你喜欢

转载自blog.csdn.net/JavaSupeMan/article/details/116656996