一:线程的生命周期
- 新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。
- 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已经具备了运行的条件,只是没分配道CPU资源。
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能。
- 阻塞:在某种特殊的情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态。
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束。
二:线程的同步
(1)同步代码块
A:同步代码块处理实现Runnable接口线程安全问题
/**
* 例子:创建三个窗口卖票,总票数为100张.
* 1.问题:卖票过程中,出现了重票,错票——>出现了线程安全问题。
* 2.问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
* 3.解决方式:当一个线程a在操作共享数据时,其他线程不能参与进来,直到线程a操作完成,其他线程才可以开始操作共享数据。
* 这种情况即使线程a出现了阻塞,也不能被改变。
* 4.在Java中,我们通过同步机制,来解决线程的安全问题。
* 方式一:同步代码块
* synchronized(同步监视器){
* //需要被同步的代码
* }
* 说明:1.操作共享数据的代码,即为需要被同步的代码-->不能将代码包含多了,也不能包含少了
* 2.共享数据:多个线程共同操作的变量
* 3.同步监视器,俗称:锁。任何一个“类对象”(是对象,不能是基本数据类型),都可以充当锁。
* 锁的要求:多个线程必须要共用同一把锁。
* 方式二:同步方法
* 如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明为同步的。
* 5.同步的方式,解决了线程的安全问题。----好处
* 操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。----局限性
*/
class Window implements Runnable{
private int ticket = 100;
//方式1.创建锁对象
//Object obj = new Object();
@Override
public void run() {
while (true){
//同步代码块
synchronized (Window.class){//方式2:用类充当锁(类其实也是一个对象)。相当于Class clazz = new Window.class;
//synchronized (this){//方式3.可以考虑使用this关键字(具体问题具体分析):此案例中this代表唯一的Window对象(用this可以不用再单独创建一个锁对象)
//synchronized(obj){//方式1
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"窗口卖票——票号为:"+ticket);
ticket--;
}else {
break;
}
}
}
}
}
public class WindowTest {
public static void main(String[] args) {
Window window = new Window();
Thread t1 = new Thread(window);
Thread t2 = new Thread(window);
Thread t3 = new Thread(window);
t1.start();
t2.start();
t3.start();
}
}
B:同步代码块处理继承Thread类线程安全问题
class Window1 extends Thread{
/**
* static的特点。
* 1.随着类的加载而加载。
* 2.优先于对象存在。
* 3.被类的所有对象共享。(判断是否用static修饰,只需要看其是否被所有对象共享)
* 4.可以用对象名调用也可以直接通过类名调用。
* 5.static修饰的内容与类相关称之为类变量,非静态修饰的内容称之为实例变量。
*/
private static int ticket = 100;
//方式一:创建锁对象,在Thread的继承类中必须要加static关键字,因为所对象要唯一
static Object obj = new Object();
@Override
public void run() {
while (true){
synchronized (Window1.class){//方式二:用类充当锁(类其实也是一个对象)相当于Class clazz = new Window1.class;
//synchronized (this){//慎用这个方式(具体问题具体分析),此案例中因为this代表w1,w2,w3三个对象,所以锁就不是唯一的了,因此不能使用
//synchronized(obj){//方式一
if (ticket>0){
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"窗口卖票——票号为:"+ticket);
ticket--;
}else {
break;
}
}
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
Thread w1 = new Window1();
Thread w2 = new Window1();
Thread w3 = new Window1();
w1.start();
w2.start();
w3.start();
}
}
(2)同步方法
A:同步方法处理实现Runnable接口线程安全问题
/**
* 在Java中,我们通过同步机制,来解决线程的安全问题。
* 方式二:同步方法
* 如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明为同步的.
*/
class Window2 implements Runnable{
private int ticket = 100;
@Override
public void run() {
while (true){
show();
}
}
private synchronized void show(){//此时它的同步监视器是:this
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"窗口卖票——票号为:"+ticket);
ticket--;
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
Window2 window2 = new Window2();
Thread t1 = new Thread(window2);
Thread t2 = new Thread(window2);
Thread t3 = new Thread(window2);
t1.start();
t2.start();
t3.start();
}
}
B:同步方法处理继承Thread类线程安全问题
/**
* 关于同步方法的总结:
* 1.同步方法仍然涉及到同步监视器,只是不需要我们显示的声明
* 2.非静态的同步方法,同步监视器是:this
* 静态的同步方法,同步监视器是:当前类本身
*/
class Window3 extends Thread{
private static int ticket = 100;
@Override
public void run() {
while (true){
show();
}
}
//private synchronized void show(){//此案例中的同步监视器是:w1,w2,w3。所以这样不对。
private static synchronized void show(){//同步监视器:Window3.class
if (ticket>0){
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"窗口卖票——票号为:"+ticket);
ticket--;
}
}
}
public class WindowTest3 {
public static void main(String[] args) {
Thread w1 = new Window3();
Thread w2 = new Window3();
Thread w3 = new Window3();
w1.start();
w2.start();
w3.start();
}
}
(3)线程安全的单例模式之懒汉式
/**
* 一:什么是单例模式?
* 保证整个系统中一个类只有一个对象的实例,实现这种功能的方式就叫单例模式。
* 二:为什么要用单例模式?
* 1、单例模式节省公共资源
* 比如:大家都要喝水,但是没必要每人家里都打一口井是吧,通常的做法是整个村里打一个井就够了,大家都从这个井里面打水喝。
* 对应到我们计算机里面,像日志管理、打印机、数据库连接池、应用配置。
* 2、单例模式方便控制
* 就像日志管理,如果多个人同时来写日志,你一笔我一笔那整个日志文件都乱七八糟,如果想要控制日志的正确性,
* 那么必须要对关键的代码进行上锁,只能一个一个按照顺序来写,而单例模式只有一个人来向日志里写入信息方便控制,
* 避免了这种多人干扰的问题出现。
* 三:实现单例模式的思路
* 1. 构造私有:
* 如果要保证一个类不能多次被实例化,那么我肯定要阻止对象被new 出来,所以需要把类的所有构造方法私有化。
* 2.以静态方法返回实例。
* 因为外界就不能通过new来获得对象,所以我们要通过提供类的方法来让外界获取对象实例。
* 3.确保对象实例只有一个。
* 只对类进行一次实例化,以后都直接获取第一次实例化的对象。
* 四:几种单例模式的区别
* 1.饿汉模式
* 饿汉模式的意思是,我先把对象(面包)创建好,等我要用(吃)的直接直接来拿就行了。
* 2.懒汉模式
* 因为饿汉模式可能会造成资源浪费的问题,所以就有了懒汉模式,懒汉模式的意思是,我先不创建类的对象实例,等你需要的时候我再创建。
*/
//线程安全的单例模式之懒汉式
class Bank{
private Bank(){}
private static Bank instance = null;
//private synchronized static Bank getInstance(){//方式二:同步方法,此时的同步监视器是Bank.class.(效率差)
private static Bank getInstance(){
/* synchronized (Bank.class){//方式一:同步代码块(效率差)
if (instance == null){
instance = new Bank();
}
return instance;
}*/
//方式三:效率更高
if (instance == null){
synchronized (Bank.class){
if (instance == null){
instance = new Bank();
}
}
}
return instance;
}
}
public class BankTest {
public static void main(String[] args) {
}
}
/**
* //饿汉模式代码案例
* public class Singleton {
* //先把对象创建好
* private static final Singleton singleton = new Singleton();
* //私有化构造方法
* private Singleton() {
* }
* //以静态方法返回实例。其他人来拿的时候直接返回已创建好的对象
* public static Singleton getInstance() {
* return singleton;
* }
* }
*/
(4)线程的死锁问题
死锁:
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
解决方法:
专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套的同步
死锁产生的四个必要条件
- 互斥条件:资源是独占的且排他使用,进程互斥使用资源,即任意时刻一个资源只能给一个进程使用,其他进程若申请一个资源,而该资源被另一进程占有时,则申请者等待直到资源被占有者释放。
- 不可剥夺条件:进程所获得的资源在未使用完毕之前,不被其他进程强行剥夺,而只能由获得该资源的进程资源释放。
- 请求和保持条件:进程每次申请它所需要的一部分资源,在申请新的资源的同时,继续占用已分配到的资源。
循环等待条件:在发生死锁时必然存在一个进程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个进程等待环路,环路中每一个进程所占有的资源同时被另一个申请,也就是前一个进程占有后一个进程所申请地资源。
以上给出了导致死锁的四个必要条件,只要系统发生死锁则以上四个条件至少有一个成立。事实上循环等待的成立蕴含了前三个条件的成立,似乎没有必要列出然而考虑这些条件对死锁的预防是有利的,因为可以通过破坏四个条件中的任何一个来预防死锁的发生。
死锁预防
我们可以通过破坏死锁产生的4个必要条件来 预防死锁,由于资源互斥是资源使用的固有特性是无法改变的。
- 破坏“不可剥夺”条件:一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到 系统的资源列表中,可以被其他的进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行。
- 破坏”请求与保持条件“:第一种方法静态分配即每个进程在开始执行时就申请他所需要的全部资源。第二种是动态分配即每个进程在申请所需要的资源时他本身不占用系统资源。
破坏“循环等待”条件:采用资源有序分配其基本思想是将系统中的所有资源顺序编号,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。
死锁解除:
一旦检测出死锁,就应立即釆取相应的措施,以解除死锁。
死锁解除的主要方法有:
1) 资源剥夺法。挂起某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程。但应防止被挂起的进程长时间得不到资源,而处于资源匮乏的状态。
2) 撤销进程法。强制撤销部分、甚至全部死锁进程并剥夺这些进程的资源。撤销的原则可以按进程优先级和撤销进程代价的高低进行。
3) 进程回退法。让一(多)个进程回退到足以回避死锁的地步,进程回退时自愿释放资源而不是被剥夺。要求系统保持进程的历史信息,设置还原点。Lock(锁):
从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当。
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁是提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
- ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显示加锁、释放锁。
Lock锁解决线程安全问题案例:
import java.util.concurrent.locks.ReentrantLock;
/**
* 解决线程安全问题的方式三:Lock锁---JDK 5.0新增。
* 1.面试题:synchronized与Lock的异同
* 相同:都可以解决线程安全问题
* 不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器。
* Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())。
* 2.优先使用顺序
* Lock > 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体之外)
*/
class NewWindow implements Runnable{
private int ticket = 100;
//1.实例化ReentrantLock
private ReentrantLock rl = new ReentrantLock();
@Override
public void run() {
while (true){
try{
//2.调用锁定方法lock()
rl.lock();
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":售票,票号为:"+ticket);
ticket--;
}else {
break;
}
}finally {
//3.调用解锁方法unlock()
rl.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
NewWindow nw = new NewWindow();
Thread t1 = new Thread(nw);
Thread t2 = new Thread(nw);
Thread t3 = new Thread(nw);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
三:线程的通信
案例:
/**
* 线程通信案例:使用两个线程打印1-100,线程1、线程2交替打印.
* 涉及到的三个方法:
* wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器
* notify():一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程被wait,就唤醒优先级高的那个,
* notifyAll():一旦执行此方法,就会唤醒所有被wait的一个线程。
* 说明:
* 1.这三个方法必须用在同步代码块或同步方法当中。
* 2.这三个方法的调用者只能是同步代码块或同步方法当中的同步监视器。否则会出现异常。
* 3.这三个方法定义在java.lang.Object类当中
* 面试题:sleep() 和 wait()的异同。
* 相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态
* 不同点:
* 1.两个发放的声明位置不同:Thread类中声明sleep(),Object类中声明wait()。
* 2.调用的要求不同:sleep()可以在任何需要的场景下调用,wait()必须被同步代码块或同步方法当中的同步监视器调用。
* 3.关于释放释放同步监视器的问题:如果两个方法都用在同步代码块或同步方法当中,sleep()不会释放同步监视器,而wait()则会释放同步监视器。
*/
class Number implements Runnable{
private int num = 1;
@Override
public void run() {
while (true){
synchronized (this){
//旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程被wait,就唤醒优先级高的那个,
notify();
if (num <= 100){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"打印出:"+num);
num++;
//使得调用如下wait()方法的线程进入阻塞状态。执行wait()时会自动释放锁
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
break;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
四:JDK 5.0新增线程创建方式
(1)实现Callable接口
与使用Runnable相比,Callable功能更强大一些。
- 相比run()方法,可以有返回值(重写call()方法)
- call()方法可以抛出异常,被外面的操作捕获,获取异常信息
- Callable支持泛型的返回值
需要借助FutureTask类,比如获取返回结果
- Future接口
- 创建多线程的方式三:实现Callable接口—-JDK 5.0新增
*/
//1.创建一个实现Callable的实现类
class NumCallable implements Callable
{ //2.实现call()方法,将此线程需要执行的操作声明在call()中 @Override public Integer call() throws Exception { int sum = 0; for (int i = 1; i <= 100; i++) {
} return sum; } } public class CallableTest { public static void main(String[] args) { //3.创建Callable接口的实现类对象 NumCallable nc = new NumCallable(); //4.将此Callable接口的实现类的对象传递到FutureTask构造器中,创建FutureTask的对象。 FutureTaskif (i%2==0){
System.out.println(Thread.currentThread().getName()+i);
sum=sum+i;
}
futureTask = new FutureTask<>(nc); //5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread类对象,并调用start(). new Thread(futureTask).start(); try {
} catch (InterruptedException e) {//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
Integer o = futureTask.get();
System.out.println("返回值是"+o);
} catch (ExecutionException e) {e.printStackTrace();
} } } ```e.printStackTrace();
(2)使用线程池(开发当中常用这个)
背景:
经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。思路:
提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,可以避免频繁的创建销毁、实现重复利用。好处:
提高响应速度(减少了创建新线程的时间)。
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)。
便于线程管理。
void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable.
Future submit(Callable task):执行任务,有返回值,一般用来执行Callable。 - void shutdown():关闭连接池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
- Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
- Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池
- Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
- Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行
案例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 线程创建的方式四:使用线程池。
*/
class NumberThread implements Runnable{
@Override
public void run() {
for (int i = 1; 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 = 1; i <= 100; i++) {
if (i%2==1){
System.out.println(Thread.currentThread().getName()+"---"+i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1.提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//强制类型转换(因为此时的service是一个接口,其中方法太少,所以要把它转换成一个实现类)
ThreadPoolExecutor service1 = (ThreadPoolExecutor)service;
//2.执行指定的线程的操作,需要提供实现Runnable接口或Callable接口实现类的对象
service1.execute(new NumberThread());//适合适用于Runnable
service1.execute(new NumberThread1());//适合适用于Runnable
//service.submit(new Callable);//适合适用于Callable
//3.关闭线程池
service1.shutdown();
}
}