多线程基础
本文主要是本人这段时间对多线程相关知识的学习总结,如有任何问题都可以评论区留言。
文章目录
一、线程相关概念
并发与并行
- 并发是指同一个时间段就一个线程在运行,就比如线程1先抢到了cpu的执行权先运行,运行了一段时间后,cpu执行权被线程2抢了,这时线程1停止运行,线程2开始运行,过了一段时间线程1又把cpu的执行权抢回来了,又开始运行线程1。
- 并行是指同一个时间段,多个线程同时运行。
进程与线程
- 进程是进入内存运行的一个应用程序
- 线程是进程中的一个执行单元,一个进程可以开启多个线程,每个线程都完成自己的任务。
线程调度
- 分时调度:多个线程轮流使用cpu的执行权,cpu平均分配各个线程的运行时间。
- 抢占式调度:各个线程有自己的优先级,优先级高的线程更有可能先运行,优先级相同就随机运行。java属于抢占式调度。个人理解:虽然网上各种说法都是优先级高的线程先运行,优先级低的线程后运行,但我感觉是优先级高的线程只是更有可能获得cpu的执行权,我测试了一下,开一个主线程和一个新线程,默认优先级都是5,都各自循环输出一句话,但改变了其中一个优先级,测试了几次,也不是每次都是我们预期的优先级高的线程先运行。
二、基本创建多线程方式
在JDK1.5之前,多线程的创建方法主要有继承Thread类和Runnable接口。
Thread类
创建一个类,继承Thread类,重写其中的run()方法。启动新线程是创建一个继承了这个类对象,调用start()方法。
常用构造方法:
- 在创建对象时传一个Runnable接口的实现类对象。
- 或者是在加一个String 在创建时就指定线程的名字
常用方法:
run()
线程体的业务逻辑代码就写在run()方法中start()
启动线程sleep(long millis)
使线程睡眠,放弃cpu的执行权,进入阻塞状态,当设定的时间结束后如果cpu处于空闲状态就运行,如果不是就转为就绪状态,等待cpu的执行权。getName()
获得当前线程的名字currentThread()
返回当前正在执行线程的对象引用。join()
用在启动线程的下面,表示当前线程执行完后才执行其他线程。yield()
线程的让步 让当前线程从运行状态变为就绪状态,放弃cpu的执行权。但也有可能,虽然现在运行了yield()方法,但下一个线程运行还是这个线程。setDaemon(true)
将当前线程设置为守护线程,注意,如果当前进程中的所有线程都是守护线程了,该进程就会停止,但不会立刻停止,还会运行一会儿守护线程。
sleep()和yield()方法的区别:sleep()使当前正在运行的线程进行阻塞状态,不考虑接下来运行线程的优先级,而yield()使当前正在运行的线程进入就绪状态,但这考虑了优先级,基本上这是给优先级比当前线程高的线程运行的机会。
注意点:
- start()方法不能调用多次
- 多个线程之间的切换是随机切换的,执行顺序是随机的。
- 启动线程运行run方法不能直接使用对象.run() 而是要使用对象.start() 如果是直接调用run()其实还是一个单线程,就是一个普通的方法调用。
- 不要使用stop()方法suspend()方法,因为如果使用stop()方法停止当前线程,它会强制中断线程的执行,并且还会释放当前线程持有的所有锁对象,这样其他等待锁对象的线程就会获得锁对象,执行可能造成线程安全的代码。举个简单的例子,你给别人转账,你的钱扣了,这个时候运行了stop()方法转账的线程就强行停止了,别人的账户里钱缺没有加上。而suspend()方法则是停止线程的执行但是不会释放锁对象的持有权,这样你一直不释放,别的线程又一直在等。总之stop()方法可能造成线程安全问题,suspend()则是会造成死锁问题。
Runnable接口
创建线程的第二种方法,具体实现是创建一个类,实现Runnable接口,实现接口中的run()方法,将该线程具体的业务逻辑代码写入run()方法中,启动该线程的方式是,new Thread(实现类对象).start()
Runnable接口和Thread类的区别
- 第一个最明显的区别就是一个接口一个类,类只能单继承,接口确可以多实现。如果我这个线程类还想继承其他类,就没办法继承其他类了。
- 使用Runnable很容易实现多个线程间的资源共享。
- 线程池不能放继承了Thread类的类,缺能方法Runnable和Callable。
三、线程的安全
当多个线程共同操作一个资源的时候就可能造成线程安全问题,就比如两个线程都要对共享资源进行加1的操作,线程一在读取共享资源,还没有运行完,然后线程二抢到了cpu的执行权,读取了共享资源的,进行修改,线程一运行完 进行修改。结果就是两个线程都运行了+1的运算,结果是共享资源仅仅就加了一。这是因为之后运行的线程把上一个线程运行的结果覆盖了。解决方法是给访问了共享资源的代码加锁。就同一个时刻只能让一个线程对共享资源进行修改,其他线程只能等着。
同步代码块
synchronized(锁对象){
访问共享资源的代码
}
注意:必须保证多个线程间 锁对象使用的是同一个。一般是使用this,表示执行这段代码的对象。
同步方法
在创建一个方法时,添加synchronized关键字,这个锁对象是调用该方法的对象。
静态同步方法
这个锁对象是类名.class
四、生产者消费者问题
多个线程处理同一个共享资源,但处理的动作不相同。
具体方法是:
wait() notify() notifyAll()
注意点:
- wait() notify()必须由同一个对象来调用,因为一个线程如果调用notify()只能唤醒使用同一个锁对象调用wait()方法的线程。
- 这三个方法属于Object类,锁对象可以是任意对象,任意对象都继承Object。
- 这三个方法只能用在synchronized修饰的地方,因为这些方式是要锁对象来调用的。
- 虚假唤醒问题
wait()和sleep()的区别
- wait()运行后线程就没有了锁对象的拥有权,而sleep()还有
- wait()是Object类 sleep是Thread类
- wait()只能在同步代码块同步方法的地方使用,sleep任何地方都可以用
五、JDK1.5之后出现的技术
Callable接口
该接口是创建线程的第三种方式,自定义一个类实现该接口,该接口有泛型,重写接口中call()方法,这个方法有返回值还有异常。
FutrureTask类
Callable接口创建的线程需要靠该类来启动,该类实现Runnable接口,构造方法中可以传一个Runnable或Callable接口实现类对象,因为它实现了Runnable接口,所以启动方式还是new Thread(Future).start()
。
该类中有一个get()方法 是用来获取Callable接口中call()方法的返回值的。
注意:必须要等Callable接口实现类中call()方法运行完后,futureTask.get()才能获取到值,不然会阻塞。
集合线程安全问题
首先回忆一下常见不安全的集合有Arraylist、HashSet、HashMap。。。常见安全的集合有Vector、Hashtable。
解决集合线程安全的方法
- 在使用时就使用常见的线程安全的集合
- 使用Collections工具类中的synchronizedList(List) 方法
- 使用CopuOnWriteArrayList类、CopuOnWriteArraySet类、ConcurrentHashMap类新建集合
ConcurrentHashMap和Hashtable的区别
Hashtable运行效率很慢,Concurrent采用的是分段锁,默认16段锁,jdk1.8后采用CAS算法。
Lock接口
常用方法:lock() unLock() tryLock()
ReentrantLock类
是Lock接口的实现类对象,构造方法可以是空参,也可以传一个boolean表示是否创建公平锁。
ReentrantLock和synchronized的区别
- ReentrantLock需要手动释放锁对象,synchronized可以手动释放
- ReentrantLock可以创建公平锁
CountDownLatch类
构造方法中传一个int,在多个线程运行结束之前,其他线程要一直等待,基本用法就是,要求的多个线程,执行结束的时候就调用一次countDown()方法。其他线程一直卡在countDownLatch.await()这里,直到要求的几个线程都调用了countDown()方法。
常用方法:countDown() await()
Semaphore类
计数信号量, 只有获取到信号量的线程才会执行 如果没有就要等待
构造方法:传一个int 表示多少个信号量 传一个int boolean 代表信号量和是否公平锁
常用方法为:acquire()获取信号量 release()释放信号量
Condition接口
同步监视器,类似于线程同步Object对象中的wait() notify() notifyAll()
而Condition接口中的方法为await() signal() signalAll()
他们两者的区别就是 一个应用在synchronized 一个应用在 Lock。具体案例还是生产者与消费者。
该接口比较特殊,创建对象的方式为:Condition con = ReentrantLock.newCondition();
volatile关键字
首先它是使用在共享资源 也就是成员变量的位置,
作用:
- 保证内存可见性
- 不保证原子性
- 禁止指令重排
volatile和synchronized的区别
- volatile不具备互斥性, 一个线程访问共享变量 , 其他线程也可以访问共享变量。synchronized是互斥锁, 具备互斥性, 在被锁的代码块上只能有一个线程访问共享变量
- volatile不具备原子性,一个线程在进行一组操作中还没完成时, 其他线程也能进入这组操作对共享变量进行修改。
- volatile是轻量级的同步策略, 可以修饰基本类型的变量,如int;synchronized是重量级的同步策略,基于对象的同步锁
相关概念
原子性
操作不可再分割的。能够分步进行的就不是原子性,
就比如1+1+1;这其实是1+1=2 2+1=3 中间分了一个2。1+1是原子性 1+1+1就不是。
i++也不是原子性 它分为 temp = i ; i=i+1; i=temp;
++i分为 i=i+1; temp = i ; i=temp;
一般而言赋值操作是原子性的,但是排除 long 和 double,为什么?因为他们俩都是64位的,在进行分配内存的时候,不会直接分配那么大的内存,所以分两次分内存,一次分配32 位。这一点在java编程思想第四版一书中也有提到。
CAS算法
保证原子性:
线程一从主存中读取共享资源的副本,然后对这个副本进行操作,操作完成后判断主存中的共享资源是否还和读取时的资源一样,判断有没有被其他线程修改;如果主存中资源没有被修改就将刚刚操作后的副本推送给主存修改共享资源;如果被修改了就重新读取,创建一个修改后的副本,对这个新副本在进行 一遍操作,再判断 如此循环。
ABA问题:就是当线程1 操作完了共享资源的副本后,判断读取时副本值和当前共享资源值。这时的共享资源已经被其他线程操作并修改了,然后又经过一些计算使这个共享资源又变成原始结果了,就相当于某个变量加了1,然后又减了1,虽然结果还是之前的但其实已经修改了。结果方法是:在共享资源加一个版本号,如果其他线程操作了版本号就会更改,条件判断时除了判断值还要判断版本号。
线程怎么保证 读取时副本值和当前共享资源值判断完后,更改共享资源前。共享资源被另一个线程修改问题。在最底层代码有lock cmpxchg 指令
这行代码将判断 赋值 这步操作锁住,让其他线程在这个时候无法修改共享资源。
原子类
JDK1.5出现,常见的原子类如下:
java.util.concurrent.atomic.AtomicBoolean
java.util.concurrent.atomic.AtomicInteger
java.util.concurrent.atomic.AtomicIntegerArray
java.util.concurrent.atomic.AtomicLong
java.util.concurrent.atomic.AtomicLongArray
java.util.concurrent.atomic.AtomicReference<V>
java.util.concurrent.atomic.AtomicReferenceArray<E>
其中常用的方法是
方法 | 说明 |
---|---|
getAndIncrement() | 以原子方式将当前值加 1 |
getAndDecrement() | 以原子方式将当前值减 1 |
incrementAndGet() | 以原子方式将当前值加 1 |
decrementAndGet() | 以原子方式将当前值减 1 |
getAndAdd(int delta) | 以原子方式将给定值与当前值相加。 |
addAndGet(int delta) | 以原子方式将给定值与当前值相加。 |
注意 看这些方法是get在前 还是get在后
线程池
如果我们想使用一个线程就去创建一个线程,并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
线程池其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
创建方式
Executors创建线程池的方法
- 创建一个只有一个线程的线程池
newSingleThreadExecutor()
- 创建一个nThreads个线程的线程池
newFixedThreadPool(int nThreads)
- 创建一个可伸缩线程的线程池
newCachedThreadPool()
在底层源码中,newCachedThreadPool()的maximumPoolSize最大线程数是Integer.MAX_VALUE;newFixedThreadPool(int nThreads)的BlockingQueue阻塞队列是Integer.MAX_VALUE。这是有一些问题的。
自定义创建线程池
ThreadPoolExecutor
其中构造方法参数为核心线程数、最大线程数、超时时间、超时单位、阻塞队列、线程工厂、拒绝策略。
简单创建线程池的代码如下:
public static void main(String[] args) {
int coreThreadSize = 5; // 核心线程池大小
int maxThreadSize = 5; // 最大核心线程池大小
long keepAliveTime = 3; // 超时时间
BlockingDeque<Runnable> blockingDeque = new LinkedBlockingDeque<Runnable>(5); // 阻塞队列
ThreadFactory threadFactory =Executors.defaultThreadFactory();// 线程工厂
AbortPolicy abortPolicy = new AbortPolicy(); // 默认拒绝策略。
// 创建线程池。
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(coreThreadSize, maxThreadSize, keepAliveTime,
TimeUnit.SECONDS, blockingDeque, threadFactory,abortPolicy);
启动线程池
threadPoolExecutor.execute(new Runnable(){ ……})
execute()方法是Executor接口中的方法;ExecutorService接口又继承了Executor接口;ExecutorService接口中有很多操作线程池的方法 比较常见的就是shutdown()关闭线程池;AbstractExecutorService类实现了ExecutorService接口,并空实现了其中的方法;接下来就是我们主要创建线程池的类ThreadPoolExecutor,它继承了AbstractExecutorService类,所以它可以调用execute()方法来启动线程。
拒绝策略
// AbortPolicy拒绝策略:全满了,还有线程进入的话,不处理,抛出异常。
AbortPolicy abortPolicy = new AbortPolicy();
// CallerRunsPolicy拒绝策略:哪里来的回哪里
CallerRunsPolicy callerRunsPolicy = new CallerRunsPolicy();
// DiscardPolicy拒绝策略:满了,会抛弃任务,但是不抛异常。
DiscardPolicy discardPolicy = new DiscardPolicy();
// DiscardOldestPolicy拒绝策略:满了,会尝试丢弃第一个,如果第一个没结束,会抛弃任务,但是也不会抛异常。
DiscardOldestPolicy discardOldestPolicy = new DiscardOldestPolicy();
AbortPolicy 是默认的。
总结
这些虽然都还只是入门,但也值得去好好学。这里有很多知识点,当时学习我都并不是很了解,也在网上找了很多资料,然后再加上自己的理解写出来的,这里面有一些对概念性的讲解也引用了一些别人的话。