线程
并发与并行
并行:多个任务同时执行
并发:多个任务同时请求,一次执行一个,交替执行
进程与线程
进程:指内存中运行的应用程序,每个进程有他自己独立的内存空间,一个程序可以同时运行多个进程,其实就是一个.exe文件
线程:进程的执行单元,一个进程可以同时并发多个线程
一个Java程序就是一个进程,所以Java里面元就的就是多线程并发
多线程的原理
在我们Java中,默认有2条线程,mian和垃圾回收线程,存在于栈区,如果我们创建一条新线程,就会在栈区新开辟一块空间,执行里面的程序,会和其他线程交替运行。
Thread类
Thread类可以理解为线程类,我们要创建线程,要么使用他的子类对象,要么创建他的对象并传进Runnable类对象。
构造方法:
public Thread():分配一个新的线程对象。
public Thread(String name):分配一个指定名字的新的线程对象。
public Thread(Runnable target):分配一个带有指定目标新的线程对象。
public Thread(Runnable target,String name):分配一个带有指定目标新的
线程对象并指定名字。
常用方法:
public String getName():获取当前线程名称。
public void start():导致此线程开始执行; Java虚拟机调用此线程的run方法。
public void run():此线程要执行的任务在此处定义代码。
下面两个是静态方法,通过类名.方法名调用
public static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
public static Thread currentThread():返回对当前正在执行的线程对象的引用。
创建多线程
-
方式一:
新建一个任意类,继承Thread,使用时,将想要执行的代码放在run()方法内,再创建该类的对象,使用start()方法可以创建新线程,并执行run()方法 -
方式二:
创建一个Runnable接口的实现类对象,将需要执行的代码放在此实现类的run()方法内。
再创建一个Thread类的对象,将创建的Runnable接口的实现类对象传入Thread的构造方法。
使用时,通过Thread类对象的start()方法创建线程并执行Runnable接口实现类的run()方法。 -
方式三:
创建Thread类对象的时候,直接在构造方法里传入Runnable接口的匿名内部类,在匿名内部类的run()方法内写要执行的代码。
然后可以使用该Thread类对象的start()方法创建线程并执行匿名内部类的run()内的代码
线程安全
如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
当我们使用多个线程访问同一资源的时候,且多个线程中对资源有改写的操作,就容易出现线程安全问题。
这时我们要做到线程安全就要做到线程同步
要做到线程同步,有三种方式可以完成
同步代码块
synchronized关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问
格式:
synchronized(同步锁){
需要同步操作的代码
}
同步锁 :锁对象可以是任意对象,但是如果要实现多个线程同步,需要使用同一个同步锁(同一个对象)
在任何时候,最多允许一个线程拥有同一个同步锁,谁拿到(随机)锁就进入代码块,其他的线程只能在外等着(BLOCKED)
同步方法
同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
注意:如果是同步方法,线程必须执行完该方法才会释放锁
格式:
public synchronized void method(){
可能会产生线程安全问题的代码
}
同步方法其实也有使用同步锁
对于非static方法,同步锁就是this
static方法,就是类名.class(当前方法的类名)
也就是说如果同步代码块想要和同步方法做到一起同步,同步锁处就可以写上上诉两个锁
Lock锁
其实java里有机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象
Lock锁也称同步锁,加锁与释放锁方法如下:
public void lock():加同步锁。
public void unlock():释放同步锁。
注:使用前需要新建一个同步L锁(Lock)对象,使用该对象执行方法(需使用他的实现类,Reentrantock来创建)
格式:
Lock lo =new ReentrantLock()
lo.lock(); //加同步锁
lo.unlock(); //释放同步锁
lo.newCondition() //创建lock锁的锁对象(Condition类型),可以创建多个不同对象,可以用不同对象执行睡眠和唤醒操作,达到选择性通知
=====下面是Condition对象的方法========
对象名.await() //当前线程进入睡眠状态,可传入指定时间,和时间的单位
对象名.signal() //唤醒使用同一个对象执行await进入睡眠状态的线程(最先的执行的)
对象名.signal() //唤醒全部使用同一个对象执行await进入睡眠状态的线程
同步方法、同步代码块是用的一种机制,Lock锁用的又是一直机制,机制不同,不能实现同步。
线程状态
当线程被创建了后,他既不是已启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有六种线程状态:
- 1.NEW(新建):线程刚被创建,但是并未启动。还没有调用start方法
- 2.Runnable(可运行):现成在虚拟机中的状态,可能在运行,也可能没在运行,取决于操作系统处理器
- 3.Blocked(锁阻塞):当一个线程被加了锁,又没有抢到锁对象,该线程就变成了锁阻塞状态(Blocked),如果该线程抢到了锁,就会进入可运行(Runnable)状态
- 4.Waiting(无限等待):当一个线程被加了锁,被锁对象执行了wait()方法,就会进入无限等待状态,必须等待另一个线程的锁对象调用notify或者notifyAll方法才能够唤醒,被唤醒后要看有没有抢到锁对象,没有抢到会进入所阻塞,抢到就进入可运行
- 5.TimedWaiting(计时等待):同Waiting状态,只是有倒计时,倒计时到了或者被其他线程唤醒就会进入抢锁对象状态(如果有)
- 6.Teminated(被终止):线程(run方法)运行完毕,或者没有捕获的异常终止了run方法
等待唤醒机制
线程间的通信
如果我们有多个线程在处理同一个资源,但是各自的动作又不相同。因为java中,切换线程是随机的,我们不能确认谁能抢到线程,但是又想让不同线程按照我们的规律配合执行。那么我们就要处理线程间的通信,这种手段就是—等待唤醒机制
等待唤醒机制
通俗来讲,等待唤醒机制就是一个线程在完成规定的操作后,就进入等待状态(wait()),这时他会暂停运行,需要等待其他线程执行唤醒 操作(notify()),才能继续执行。(如果需要一次性唤醒多个线程,可以使用notifyAll())
wait/notify就是线程间的一种协作机制。
等待与唤醒的方法
格式:
同步锁.wait() //使当前线程进入等待状态
同步锁.notify() //使最先进入等待状态的线程苏醒
同步锁.notifyAll() //使所有进入等待状态的线程苏醒
注意:这两个方法必须在同步代码中执行,并且使用同步锁对象来调用
线程池
线程池,可以理解一个装线程的池子,池子里的线程可以反复利用,当有多个任务需要使用线程的时候,如果线程池中还有空闲线程,就会分配给任务去调用,当任务执行完毕后,线程会回到线程池,可供其他任务继续重复调用。没有分配到线程的任务,就会进入等待状态,等待被调用执行任务的线程回来。
好处:
- 降低资源消耗:减少了创建和销毁线程的次数,一个线程反复利用
- 提高响应速度:任务可以不需要等待线程创建,直接调用使用
- 提高线程的可管理性:可以根据系统的承受能力,自己调整线程池中的线程数,防止同时创建了过多的线程
创建线程池
我们使用线程池的顶级接口是Executor,但是严格意义上来讲,它只是一个执行线程的工具,真正的线程池接口是ExecutorService,因为要配置一个线程池是比较复杂的,所以现阶段我们可以先使用Executors线程工具类,用里面的静态方法直接创建一些常用的线程池
格式:
public static ExecutorService newFixedThreadPool(int 线程数)//创建一个自己指定线程数量,类型为ExecutorService接口实现对象的线程池。
使用线程池
想要使用线程池,我们可以使用ExecutorService接口中的方法来使用
格式:
线程池对象.submit(Runnable task) //该方法可以获取线程池中的某一个线程对象,并执行Runnable实现类中的run方法
线程池对象.shutdown() //关闭该线程池,一般不用