多线程是实现并发机制的一种有效手段。进程和线程一样,都是实现并发的一个基本单位。线程是比进程更小的执行单位,线程是在进程的基础上进行的进一步划分。所谓多线程是指一个进程在执行过程中可以产生多个线程,这些线程可以同时存在、同时运行,一个进程可能包含了多个同时执行的线程。
同步:排队执行,效率低但安全
异步:同时执行,效率高但数据不安全
并发:指两个或多个事件在同一个时间段内发生。
并行:指两个或多个事件在同一时刻发生(同时发生)。
步骤:
创建一个自定义类并继承Thread类;
重写run()方法,创建新的执行任务(通过thread对象的start()方法启动任务,一般不直接调用run()方法)
创建自定义类对象实例,调用start(),让线程执行
代码如下:
//MyThread.java public class MyThread extends Thread{ @Override public void run() { //run()方法就是线程要执行的任务的方法 for (int i = 0; i < 10; i++) { System.out.println("MyThread" + i); } } } //ThreadTest.java public class ThreadTest { public static void main(String[] args) { MyThread mt = new MyThread(); mt.start(); //启动线程任务 for (int i = 0; i < 5; i++) { System.out.println("MainThread" + i); } } }运行结果:
可以看到顺序并不统一,两个线程在交替执行而且各自所占的时间不完全相同,这是线程在抢时间片,谁先抢到谁就执行。
时序图:
运行过程中子线程任务中调用的方法都在子线程中运行
在上述代码中。如果Thread对象只需要调用1次,也可以通过使用匿名内部类的方式进行简化:
public class ThreadTest { public static void main(String[] args) { new Thread(){ public void run() { for (int i = 0; i < 5; i++) { System.out.println("MyRunnable" + i); } } }.start(); for (int i = 0; i < 5; i++) { System.out.println("MainThread" + i); } } }Runnable接口代码:
public interface Runnable { public abstract void run(); }步骤:
创建一个自定义类实现Runnable接口,并实现其抽象方法run(),编写线程要执行的任务创建自定义类对象实例用Thread类创建一个对象实例,并将第二步中的自定义类对象实例作为参数传给其构造函数调用Thread类实例的start()方法执行线程。 //MyRunnable.java public class MyRunnable implements Runnable { @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println("MyRunnable" + i); } } } //RunnableTest.java public class RunnableTest { public static void main(String[] args) { MyRunnable mr = new MyRunnable(); Thread t = new Thread(mr); t.start(); for (int i = 0; i < 5; i++) { System.out.println("MainRunnable" + i); } } } //运行效果应该跟上面继承Thread类实现多线程效果差不多。上述代码也可以通过使用匿名内部类的方式进行简化:
public class RunnableTest { public static void main(String[] args) { new Thread(new Runnable() { public void run() { for (int i = 0; i < 5; i++) { System.out.println("MyRunnable" + i); } } }).start(); for (int i = 0; i < 5; i++) { System.out.println("MainRunnable" + i); } } }上面两种方式的比较
继承Thread类:
优点:直接使用Thread类中的方法,代码简单弊端:如果已有父类,不可用(Java不可多继承)实现Runnable接口(更常用):
与继承Threadl类相比具有以下优势:
通过创建任务,给线程分配任务实现多线程,更适合多个线程同时执行相同任务的情况可以避免单继承带来的局限性(Java允许实现多个接口,但不能继承多个父类)任务和线程分离,提高程序健壮性后续学到的线程池技术,它只接收Runnable类型任务,不接收Thread类型线程Thread类API
常用构造方法 构造器描述Thread()分配新的 Thread对象。Thread(Runnable target)分配新的 Thread对象。Thread(Runnable target, String name)分配新的 Thread对象。Thread(String name)分配新的 Thread对象。 常用其他方法 变量和类型方法描述longgetId()返回此Thread的标识符。StringgetName()返回此线程的名称。intgetPriority()返回此线程的优先级。voidsetPriority(int newPriority)更改此线程的优先级。Thread.StategetState()返回此线程的状态。static ThreadcurrentThread()返回对当前正在执行的线程对象的引用。voidstart()导致此线程开始执行; Java虚拟机调用此线程的run方法。static voidsleep(long millis)导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数,具体取决于系统计时器和调度程序的精度和准确性。static voidsleep(long millis, int nanos)导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数加上指定的纳秒数,具体取决于系统定时器和调度程序的精度和准确性。voidsetDaemon(boolean on)将此线程标记为 daemon线程或用户线程。 特殊字段:控制线程抢到时间片的几率 变量和类型字段描述static intMAX_PRIORITY线程可以拥有的最大优先级。static intMIN_PRIORITY线程可以拥有的最低优先级。static intNORM_PRIORITY分配给线程的默认优先级。其他的可以参考Java的API手册
Callable接口代码:
public interface Callable<V> { V call() throws Exception; }步骤:
创建一个自定义类实现Callable接口,并实现其抽象方法call(),编写线程要执行的任务
class XXX implements Callable<T> { @Override public <T> call() throws Exception { return T; } }创建FutureTask对象 , 并传入第一步编写的Callable类对象
FutureTask<Integer> future = new FutureTask<>(callable);通过Thread,启动线程
new Thread(future).start(); //MyCallable.java import java.util.concurrent.Callable; public class MyCallable<T> implements Callable<T> { @Override public T call() throws Exception { for (int i = 0; i < 5; i++) { System.out.println("MyCallable:" + i); } return null; } } //CallableTest.java import java.util.concurrent.FutureTask; public class CallableTest { public static void main(String[] args) { MyCallable<String> mc = new MyCallable<> (); FutureTask<String> future = new FutureTask<> (mc); new Thread(future).start(); for (int i = 0; i < 5; i++) { System.out.println("main" + i); } } }上述代码也可以通过使用匿名内部类的方式进行简化:
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; public class CallableTest { public static void main(String[] args) { new Thread(new FutureTask<>(new Callable<String>() { @Override public String call() throws Exception { for (int i = 0; i < 5; i++) { System.out.println("MyCallable:" + i); } return null; } })).start(); for (int i = 0; i < 5; i++) { System.out.println("main" + i); } } }Runnable 与 Callable比较
相同点:
都是接口都可以编写多线程程序都采用Thread.start()启动线程不同点
Runnable没有返回值;Callable可以返回执行结果Callable接口的call()允许抛出异常;Runnable的run()不能抛出Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执 行,如果不调用不会阻塞。
currentThread() 可以获取当前正在执行的线程对象
//MyRunnable.java public class MyRunnable implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName());//获取当前线程对象的名称 } } //GetThread.java public class GetThread { public static void main(String[] args) { System.out.println(Thread.currentThread().getName());//获取当前线程对象的名称 new Thread(new MyRunnable()).start(); new Thread(new MyRunnable()).start(); new Thread(new MyRunnable(),"answer").start(); //给线程指定一个名称 (方法一) Thread t = new Thread(new MyRunnable()); t.setName("anotherWay"); //给线程指定一个名称 (方法二) t.start(); } }执行结果:
sleep(long millis)是Thread类的静态方法,类名直接调用即可,单位ms。
public class Demo1 { public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 10; i++) { System.out.print(i + " "); Thread.sleep(1000); } } }运行结果:每隔1秒打印一个数字。
线程阻塞:所有较耗时的操作都能称为阻塞。也叫耗时操作。
一个线程是一个独立的执行路径,它是否应该结束,由其自身决定。
因为线程执行过程会有很多资源需要使用或释放,如果干涉它的结束,很可能导致资源没能来得及释放,一直占用,从而产生无法回收的内存垃圾。
Java以前提供stop()方法可以结束线程,现在已经过时(不再使用)。现在出了新的方法,给线程打中断标记(interrupt)来控制它的结束。
具体方法就是 调用interrupt()方法,子线程执行时捕获中断异常,并在catch块中,添加处理释放资源的代码。
如下代码所示:main线程执行完后不管子线程是否执行完都中断掉它
//MyRunnable.java public class MyRunnable implements Runnable{ @Override public void run() { //线程任务:打印1-10 for (int i = 1; i <= 10; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); try { Thread.sleep(1000); } catch (InterruptedException e) {//发现中断标记,进入catch块中,进行释放资源处理 System.out.println(Thread.currentThread().getName()+":发现中断标记,我自杀了"); return; //为了演示,直接结束 } } } } //InterruptTest.java public class InterruptTest { public static void main(String[] args) { Thread t1 = new Thread(new MyRunnable()); t1.setName("myThread"); t1.start(); //main线程 打印1-5 for (int i = 1; i <= 5; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } t1.interrupt(); //给线程t1添加中断标记 } }运行结果:
线程分为守护线程和用户线程
用户线程:当一个进程不包含任何存活的用户线程时,进行结束。守护线程:守护用户线程,当最后一个用户线程结束时,所有守护线程自动死亡。直接创建的都是用户线程,
设置线程为守护线程:在启动之前设置 ,语法为:线程对象.setDaemon(true);。
//MyRunnable.java public class MyRunnable implements Runnable{ @Override public void run() { //线程任务:打印1-10 for (int i = 1; i <= 10; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); return; //为了演示,直接结束 } } } } //InterruptTest.java public class InterruptTest { public static void main(String[] args) { Thread t1 = new Thread(new MyRunnable()); t1.setName("myThread"); t1.setDaemon(true);//设置t1为守护线程 t1.start(); //main线程 打印1-5 for (int i = 1; i <= 5; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }运行结果
我们先来看个例子:三个窗口(线程)同时卖5张票。
public class Demo1 { public static void main(String[] args) { Runnable run = new Ticket(); new Thread(run).start(); new Thread(run).start(); new Thread(run).start(); } static class Ticket implements Runnable{ private int count = 5; //票数 @Override public void run() { while (count > 0) { //卖票 System.out.println(Thread.currentThread().getName()+"正在卖票"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count--; System.out.println(Thread.currentThread().getName()+"出票成功,余票:"+count); } } } }运行结果部分截图:
我们看到余票出现了负数,显然这是不合理的,这就是线程不安全导致的。出现这种情况的原因:线程争抢,导致线程不安全。 多线程在进行同一卖票任务时,没人干涉,各个窗口疯狂买票,最终导致卖的票超出总票数,余票出现负数。
线程不安全的原因:
当多线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致。
临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性。原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可打乱或缺省。多个线程争抢同一个数据,使得数据在判断和使用时出现不一致的情况。那如何解决呢?
解决方法:
保证一段数据同时只能被一个线程使用(排队使用),也就是线程同步,给线程加锁(synchronized)
我们有以下三种方法解决线程不安全的问题:同步代码块、同步方法、显示锁
使用synchronized关键字加上一个锁对象来定义一段代码, 这就叫同步代码块
多个同步代码块如果使用相同的锁对象, 那么他们就是同步的
语法格式:synchronized(锁对象) {}
任何对象都可以作为锁对象存在。
还以上面卖票的代码为例,给卖票的线程加锁
以方法为单位进行加锁。把synchronized关键字修饰在方法中。
还以上面卖票的代码为例,写一个synchronized修饰的方法sale()执行卖票任务,
以上方法中,同步代码块和同步代码都是隐式锁
Lock l = new ReentrantLock():自己创建一把锁
lock():加锁 unlock():解锁
还以上面卖票的代码为例
** 显式锁和隐式锁的区别:**
区别synchronizedlock原始构成Java关键字,由JVM维护,是JVM层面的锁JDK1.5之后的类,使用lock是在调用API,是API层面的锁使用方式隐式锁,不需要手动获取和释放锁,只需要写synchronized,不用进行其他操作显式锁,需要手动获取和释放锁,如果没有释放锁,可能会出现死锁等待中断不会中断,除非抛出异常或正常运行完成可以中断,1:调用设置超时方法tryLock(long timeout ,timeUnit unit);2:调用lockInterruptibly()放到代码块中,然后调用interrupt()方法可以中断加锁公平非公平锁可以是公平锁也可以是非公平锁,默认是非公平锁。可以在其构造方法传入Boolean值,true公平锁,false非公平锁绑定多个条件没有。不能精确唤醒线程,要么随机唤醒一个线程,要么唤醒所有等待线程用来实现分组唤醒需要唤醒的线程,可以精确唤醒线程性能JDK1.5时性能较低,JDK1.6时性能优化,与lock相较无异JDK1.5时性能更高,JDK1.6时synchronized优化赶上lock加锁方式线程获取独占锁(CPU悲观锁机制),只能依靠阻塞等待线程释放锁。在CPU转换线程阻塞时会引起线程上下文切换,当竞争锁的线程过多时,会引起CPU频繁上下文切换导致效率低下使用乐观锁机制(CAS操作 Computer and Swap),假设不会发生冲突,一旦发生冲突失败就重试,直到成功为止。公平锁:先来先得,排队执行
非公平锁:抢占式的,谁抢到是谁的
更多关于线程安全的问题可以看下面这篇某大佬的文章
如果你这样回答“什么是线程安全”,面试官都会对你刮目相看(建议珍藏)
当两个或两个以上的线程在执行过程中,因为争夺资源而造成的一种相互等待的状态,由于存在一种环路的锁依赖关系而永远地等待下去,如果没有外部干涉,他们将永远等待下去,此时的这个状态称之为死锁。
多个线程相互占用对方的资源的锁,而又相互等对方释放锁,此时若无外力干预,这些线程则一直处于阻塞的假死状态,形成死锁。
举个例子,如下图所示,在线程A持有锁L并想获得锁M的同时,线程B持有锁M并尝试获得锁L,那么这两个线程将永远地等待下去,这种情况就是死锁形式。
死锁产生的条件:
互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用完释放。请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{A,B,C,···,Z} 中的A正在等待一个B占用的资源;B正在等待C占用的资源,……,Z正在等待已被A占用的资源。按顺序加锁:如果每个线程都按同一个的加锁顺序这样就不会出现死锁。
给锁加时限:每个线程获取锁的时候加上个时限,如果超过某个时间就放弃锁。
死锁检测:按线程间获取锁的关系检测线程间是否发生死锁,如果发生死锁就执行一定的策略,如终断线程或回滚操作等。
更多关于线程死锁的问题可以看下面这篇某大佬的文章,以上内容也是来自这篇文章:
多线程 死锁详解
Object方法中提供了一些线程间相互通信的方法
变量和类型方法描述voidnotify()唤醒正在此对象监视器上等待的单个线程。voidnotifyAll()唤醒等待此对象监视器的所有线程。voidwait()导致当前线程等待它被唤醒,通常是 通知或 中断 。voidwait(long timeoutMillis)导致当前线程等待它被唤醒,通常是 通知或 中断 ,或者直到经过一定量的实时。voidwait(long timeoutMillis, int nanos)导致当前线程等待它被唤醒,通常是 通知或 中断 ,或者直到经过一定量的实时。什么时候需要通信:
多个线程并发执行时, 在默认情况下CPU是随机切换线程的,如果我们希望他们有规律的执行, 就可以使用通信。
看下面代码,有Cooker类,Waiter类,Food类
厨师cooker为生产者线程,服务员waiter为消费者线程,食物food为生产与消费的物品;
假设目前只有一个厨师,一个服务员,一个盘子。理想状态是:厨师生产一份饭菜,服务员端走一份,且饭菜的属性不能发生错乱;
厨师可以制作两种口味的饭菜,制作100次;
服务员可以端走饭菜100次;
public class Demo { public static void main(String[] args) { Food f = new Food(); new Cooker(f).start(); new Waiter(f).start(); } static class Cooker extends Thread{ //生产者线程 private Food f; public Cooker(Food f){ this.f = f; } public void run() { //生产100个菜 for (int i = 0; i < 100; i++) { if (i % 2 == 0){ f.setNameAndTaste("菜1","味道1"); } else { f.setNameAndTaste("菜2","味道2"); } } } } static class Waiter extends Thread { //消费者线程 private Food f; public Waiter(Food f){ this.f = f; } public void run() { for (int i = 0; i < 100; i++) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } f.get(); } } } static class Food { private String name; private String taste; public void setNameAndTaste(String name,String taste){ //生产 this.name = name; //先设定名称 try { Thread.sleep(100); //为使线效果明显,中间休眠一段时间 } catch (InterruptedException e) { e.printStackTrace(); } this.taste = taste; //休眠后设定味道 } public void get(){ //消费 System.out.println("服务员端走的菜名称是:" + name + ",味道:" + taste); } } }运行结果
原因:我们在设定菜名和味道的setNameAndTaste方法中,先设定名称,然后休眠一段时间,再设定的味道,中间休眠的那段时间很可能发生时间片丢失,使得菜属性产生混乱。
解决方式一:
为了防止在生产过程中setNameAndTaste出现时间片切换,可以用synchronized修饰此方法;
public synchronized void setNameAndTaste(String name,String taste){ //... } public synchronized void get(){ // 消费 //... }运行结果
可以看出,依然不符合实际情况,这是因为synchronized只是确保了方法内部不会发生线程切换,但并不能保证生产一个消费一个的逻辑关系
解决方式二:
在解决方案一的基础上,进行线程之间的通信
厨师做完饭后喊醒服务员,自己睡着。服务员送完饭后喊醒厨师,自己睡着;将Food类左如下修改
运行结果,做一道菜,端走一道。
Enum Thread.State描述了六种线程的状态,如下表所示
Enum Constant描述BLOCKED线程的线程状态被阻塞等待监视器锁定。(阻塞)NEW尚未启动的线程的线程状态。(创建)RUNNABLE可运行线程的线程状态。(就绪和运行)TERMINATED终止线程的线程状态。(消亡)TIMED_WAITING具有指定等待时间的等待线程的线程状态。(有限期等待)WAITING等待线程的线程状态。(无限期等待)普通线程的执行流程:
创建线程 → 创建任务 → 执行任务 → 关闭线程
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间。 线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建和销毁线程对象的操作,节省了大量的时间和资源。
线程池的好处
降低资源消耗。提高响应速度。提高线程的可管理性。Java中有四种线程池(ExecutorService):缓存线程池、定长线程池、单线程线程池、周期性任务定长线程池
长度无限制
执行流程:
判断线程池是否存在空闲线程
存在则使用
不存在,则创建线程 并放入线程池, 然后使用
ExecutorService service = Executors.newCachedThreadPool(); //获取缓存线程池对象 //向线程池中 加入 新的任务 service.execute(new Runnable() { @Override public void run() { //线程任务代码 } });长度是指定的数值
步骤:
判断线程池是否存在空闲线程存在则使用不存在空闲线程,线程池未满的情况下,则创建线程 并放入线程池, 然后使用不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程 ExecutorService service = Executors.newFixedThreadPool(2); service.execute(new Runnable() { public void run() { //线程任务代码 } });步骤:
判断线程池的那个线程是否空闲空闲则使用不空闲则等待池中的单个线程空闲后使用 ExecutorService service = Executors.newSingleThreadExecutor(); service.execute(new Runnable() { public void run() { //线程任务代码 } });步骤:
判断线程池是否存在空闲线程存在则使用不存在空闲线程,且线程池未满的情况下,则创建线程,并放入线程池后使用不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程周期性任务执行时:定时执行, 当某个时机触发时, 自动执行某任务
ScheduledExecutorService service = Executors.newScheduledThreadPool(2); /** * 定时执行 * 参数1. runnable类型的任务 * 参数2. 时长数字 5 * 参数3. 时长数字的单位 TimeUnit.SECONDS */ service.schedule(new Runnable() { public void run() { //线程任务代码 } },5,TimeUnit.SECONDS); /** * 周期执行 * 参数1. runnable类型的任务 * 参数2. 时长数字(延迟执行的时长) 5 * 参数3. 周期时长(每次执行的间隔时间) 2 * 参数4. 时长数字的单位 TimeUnit.SECONDS */ service.scheduleAtFixedRate(new Runnable() { public void run() { //线程任务代码 } },5,2,TimeUnit.SECONDS);