线程(thread):
一个程序内部的一条执行路径。main方法的执行就是一条单独的执行路径。
多线程:
是指从软硬件上实现多条执行流程的技术。
1.多线程的创建:
方法一:继承Thread类
实现步骤:
- 先写一个Thread的子类,重写run方法。
public class MyThread extends Thread{ @Override public void run() { //run方法中写线程要执行的事情。 for (int i = 1; i <= 100; i++) { System.out.println(getName()+"已经下载了..."+i+"%"); } } }
- 创建Thread的子类对象。
- 调用start方法启动线程(自动执行run方法,不可以直接调用run方法。只能调用start方法开启线程。)
/** * 线程的第一种创建方式步骤: * 1.先写一个Thread的子类,重写run方法。 * 2.创建Thread的子类对象。 * 3,调用start方法启动线程(自动执行run方法) * (不可以直接调用run方法,只能调用start方法开启线程) */ public class Demo { public static void main(String[] args) throws InterruptedException { //创建线程对象 MyThread t1=new MyThread(); //设置线程名称 t1.setName("海贼王"); //启动线程 t1.start(); MyThread t2=new MyThread(); t2.setName("火影忍者"); t2.start(); //这里的for循环被主线程执行:主线程的名称是main。 for (int i = 0; i < 100; i++) { //线程睡眠 Thread.sleep(1000); Thread thread=Thread.currentThread(); System.out.println(thread.getName()+"已经下载了..."+i+"%"); } } }
方法二:实现Runnable接口
实现步骤:
- 定义一个Runable接口的实现类,作为线程的任务类,需要重写run方法。
public class MyRunnable implements Runnable{ @Override public void run() { for (int i = 1; i <= 100; i++) { //获取当前线程的对象:哪一个线程执行run方法,这里获取的就是哪一个线程对象 Thread thread=Thread.currentThread(); //获取线程名 String name=thread.getName(); System.out.println(name+"正在下载..."+i+"%"); } } }
- 创建线程任务的对象。
- 把线程任务的对象,交给Thread类的对象。
- 让Thread的对象调用start方法,启动线程。(会自动执行任务类中的run方法)
/** * 线程第二种创建方式的步骤: * 1.写一个Runnable接口的实现类,作为线程的任务类,需要重写run方法 * 2.创建线程任务的对象 * 3.把线程任务的对象,交给Thread类的对象 * 4.让Thread的对象调用start方法,启动线程(会自动执行任务类中的run方法) * * 优点: */ public class Demo { public static void main(String[] args) { //创建线程任务类的对象 MyRunnable mr=new MyRunnable(); //创建线程对象,把任务交给线程 Thread t1=new Thread(mr); //启动线程 t1.start(); /** *实现Runnable接口(匿名内部类形式) */ Thread t2=new Thread(new Runnable() { @Override public void run() { for (int i = 1; i <= 100; i++) { //获取当前线程的对象:哪一个线程执行run方法,这里获取的就是哪一个线程对象 Thread thread = Thread.currentThread(); //获取线程名 String name = thread.getName(); System.out.println(name + "正在下载..." + i + "%"); } } }); t2.start(); } }
优点:线程任务类只是实现了Runnable接口可以继续继承和实现。
方法三:实现Callable接口
优点:可以得到线程执行的返回值。
实现步骤
- 写一个Callable接口的实现类,重写call方法
/** * Callable接口的实现类:用来封装线程要执行的事情,重写call方法,方法执行完毕之后有返回值 * 想要返回什么类型的数据,泛型就写什么类型 */ public class MyCallable implements Callable<Integer> { @Override public Integer call() throws Exception { return 10; } }
- 创建Callable接口的实现类对象
- 创建一个FutureTask类的对象,把Callable实现类对象封装进来
- 创建Thread对象,把FutureTask对象封装进来。
- 调用Thread对象的start方法。
/** * 线程的第三种创建方式的步骤: * 1.写一个Callable接口的实现类,重写call方法 * 2.创建Callable接口的实现类的对象 * 3.创建一个FutureTask类的对象,把Callable实现类对象封装进来 * 4.创建Thread对象,把FutureTask对象封装进来 * 5.调用Thread对象的start方法。 */ public class Demo { public static void main(String[] args) throws ExecutionException, InterruptedException { //创建Callable接口的实现类的对象 MyCallable mc=new MyCallable(); //创建一个FutureTask类的对象,把Callable实现类对象封装进来 FutureTask<Integer> ft=new FutureTask<>(mc); //创建Thread对象,把FutureTask对象封装进来 Thread t1=new Thread(ft); //调用Thread对象的start方法。 t1.start(); //等线程的任务代码执行完毕了(call方法执行完了),就要返回结果,结果会封装到FutureTask对象里面 //通过FutureTask对象获取返回值。 Integer result=ft.get(); System.out.println("线程执行的结果是:"+result); } }
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
可以在线程执行完毕后去获取线程执行的结果。
缺点: 编码复杂一点
2.Thread的常用方法:
getName():获取当前线程的名称。
setName():设置当前线程的名称。
Thread currentThread():返回当前正在执行的线程对象的引用。
1.此方法是Thread类的静态方法,可以直接调用Thread类调用。
2.这个方法在哪个线程执行中调用,就会得到哪个线程对象。
sleep(long time): 指定休眠时间。单位为毫秒。
3.线程安全:
多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。
/** * 账户类:用于封装账户的金额信息 */ public class Account { private int money=10000;//余额 //取钱的方法:其实就是对账户的余额进行更新操作。 public void drawMoney(int money){ String name = Thread.currentThread().getName(); //判断余额是否足够 if (this.money>=money){ try { Thread.sleep(1000);//线程1,线程2 } catch (InterruptedException e) { e.printStackTrace(); } this.money-=money; System.out.println(name+"过来取钱了,取了"+money+"元,还剩下"+this.money+"元"); }else { System.out.println(name+"过来取钱了,可是没钱了"); } } }
public class DrawThread extends Thread{ private Account account; public DrawThread(Account account) { this.account = account; } @Override public void run() { account.drawMoney(10000); } }
/** * 线程安全问题发生的原因:多个线程同时访问同一个共享资源且存在修改该资源。 */ public class Demo { public static void main(String[] args) { Account account=new Account(); DrawThread dt1=new DrawThread(account); DrawThread dt2=new DrawThread(account); dt1.start(); dt2.start(); } }
4.线程同步:
核心思想:加锁(把访问共享资源的代码封住,只有获取锁的线程可以执行,没有获得锁就不能执行。)
1.同步代码块:
作用:把出现线程安全问题的核心代码给上锁。
锁对象规范要求:
- 规范上:建议使用共享资源作为锁对象。
- 对于实例方法建议使用this作为锁对象。
- 对于静态方法建议谁用字节码(类名.class)对象作为锁对象。
/** * 同步代码块 */ synchronized (Aaccount.class){ //判断余额是否足够 if(Aaccount.money>=money){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } //可以取钱 Aaccount.money-=money; System.out.println(name+"过来取钱,取了"+money+"元,还剩"+Aaccount.money); }else { System.out.println(name+"过来取钱,余额不足"); } }
2.同步方法:
/** * 同步方法 * 如果是实例方法:锁对象是this * 如果是静态方法:锁对象是类名.class */ public synchronized void drawMoeny(int money){ //获取线程名称,方便后面使用 String name = Thread.currentThread().getName(); //判断余额是否足够 if(Aaccount.money>=money){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } //可以取钱 Aaccount.money-=money; System.out.println(name+"过来取钱,取了"+money+"元,还剩"+Aaccount.money); }else { System.out.println(name+"过来取钱,余额不足"); } }
3.Lock锁:
为了更清晰的表达如何加锁和释放锁,Lock更加灵活,方便。Lock是接口,一般采用实现类ReentrantLock来表示锁对象。
public void drawMoeny(int money){ //获取线程名称,方便后面使用 String name = Thread.currentThread().getName(); lock.lock();//上锁 //判断余额是否足够 try { if(this.money>=money){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } //可以取钱 this.money-=money; System.out.println(name+"过来取钱,取了"+money+"元,还剩"+this.money); }else { System.out.println(name+"过来取钱,余额不足"); } } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock();//解锁 } }
5.线程通信:
所谓线程通信就是线程之间相互发送指令,从而改变线程执行状态。
如何实现:
- 多个线程之间需要有共性数据
- 线程根据共享数据的情况决定自己该怎么做,是让线程等待,还是让线程唤醒。
实际应用场景:
- 生产者和消费者模型:生产者负责生产数据,消费者负责消费生产者产生的数据。
- 要求:生产者线程生产完毕数据后唤醒消费者,然后等待自己,消费者消费玩该数据后唤醒生产者,然后等待自己。
线程通信的前提:
线程通信通常是在多个线程操作同一个共享资源的时候进行通信,且保证线程安全。
/* 账户类: 封装账户的余额信息,还有存钱和取钱的方法 */ public class Acount { //账户的余额, 默认是10万元 private int money = 100000; /** * 取钱的方法 * @param money 要取钱的金额 */ //同步方法的锁对象:this public synchronized void drawMoney(int money){ String name=Thread.currentThread().getName(); //1.判断余额是否足够 if (this.money>=money){ //2.如果余额足够,开始取钱,取完钱后,唤醒其他等待的线程 this.money-=money; System.out.println(name+"过来取了"+money+"元,还剩"+this.money+"元"); this.notifyAll(); }else { //3.如果余额不足,不能取钱,让当前线程等待 try { this.wait();//释放锁等待。 } catch (InterruptedException e) { e.printStackTrace(); } } } /** * 存钱的方法 * @param money 要存钱的金额 */ //同步方法的锁对象时: this public synchronized void saveMoney(int money){ String name=Thread.currentThread().getName(); //1.判断余额是否足够 if (this.money==0){ //2.如果余额不足,开始存钱,存完钱后,唤醒其他等待的线程 this.money+=money; System.out.println(name+"过来存了"+money+"元,还有"+this.money+"元"); this.notifyAll(); }else { //3.如果余额足够,不能存钱,让当前线程等待 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } }
public class DrawThread extends Thread{ private Acount acount; public DrawThread(Acount acount){ this.acount=acount; } @Override public void run() { for (int i = 0; i < 10; i++) { acount.drawMoney(100000); } } }
public class SaveThread extends Thread{ private Acount acount; public SaveThread(Acount acount){ this.acount=acount; } @Override public void run() { for (int i = 0; i < 10; i++) { acount.saveMoney(100000); } } }
public class Demo { public static void main(String[] args) { //创建三个存钱线程、两个取钱线程、这五个线程共享同一个账户。 Acount acount=new Acount(); SaveThread t1=new SaveThread(acount); t1.setName("爸爸"); SaveThread t2=new SaveThread(acount); t2.setName("妈妈"); SaveThread t3=new SaveThread(acount); t3.setName("哥哥"); DrawThread t4=new DrawThread(acount); t4.setName("小弟"); DrawThread t5=new DrawThread(acount); t5.setName("小妹"); t1.start(); t2.start(); t3.start(); t4.start(); t5.start(); } }
wait():当前线程等待,直到另一个线程调用notify()或notifyAll()唤醒自己
notify():唤醒正在等待对象监视器(锁对象)的单个线程
notifyAll():唤醒正在等待对象监视器(锁对象)的所有线程
6.线程池:
线程池就是一个可以复用线程的技术。
(如果用户每发起一个请求,后台就创建一个新的线程来处理,下次新任务又要创建新线程,而创建新线程的开销是很大的,这样会严重影响系统的性能。)
实现线程池:(JDk5起提供了代表线程池的接口:ExecutorService)
public class Demo1 { public static void main(String[] args) { //创建线程池对象 ThreadPoolExecutor pool=new ThreadPoolExecutor( 3, //核心线程数 5, //最大线程数 2, //临时线程存活时间 TimeUnit.SECONDS, //指定存活时间的单位 new ArrayBlockingQueue<>(10), //指定任务队列 Executors.defaultThreadFactory(), //指定用哪个线程工厂创建队列 new ThreadPoolExecutor.AbortPolicy() //指定拒绝线程策略 ); // for (int i = 1; i <= 17; i++) { int j=i; pool.submit(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()+"线程执行了.."+j+"号任务"); } }); } //关闭线程池 pool.shutdown(); } }
public class Demo2 { public static void main(String[] args) throws ExecutionException, InterruptedException { ThreadPoolExecutor pool=new ThreadPoolExecutor( 3, 5, 4, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy() ); Future<Object> submit = pool.submit(new Callable<Object>() { @Override public Object call() throws Exception { int sum = 0; for (int i = 1; i <= 100; i++) { sum += i; } return sum; } }); System.out.println(submit.get()); pool.shutdownNow(); } }
线程池常见面试题:
临时线程什么时候创键?
核心线程都在忙+任务队列满了,就会创建临时线程。
什么时候会开始拒绝任务?
核心线程都在忙+临时线程也在忙+任务队列也满了,多余的任务就会触发拒绝策略
7.并发,并行
正在运行的程序(软件)就是一个独立的进程,线程是 属于进程的,多个线程其实是并发与并行同时进行的。
并发:
CPU同时处理的线程数量有限。
CPU会轮询为系统的每个线程服务,由于CPU切换速度很快,给我们的感觉这些线程在同时执行,这就是并发。
并行:
在同一时刻上,同时有多个线程在被CPU处理并执行。
8.线程的生命周期:
线程的生命周期指的是,线程从创建到销毁的过程可能存在不同的状态。
线程的状态
线程的状态:也就是线程从生到死的过程,以及中间经历的各种状态及状态转换。
Java总共定义了6种状态
6种状态都定义在Thread类的内部枚举类中。