DAY14
案例展示:(多线程的引入)
- 一个程序只有一条执行路径的程序叫做单线程程序
- 一个程序如果有多条执行路径,那么这程序就是多线程程序
- 进程
- 正在运行的程序,是系统进行资源分配和调用的独立单位
- 每个进程都有它自己的内存空间和系统资源
- 线程
- 是进程中的单个顺序控制流,是一条执行路径
- 一个进程如果只有一条执行路径,则称为单线程程序
- 一个进程如果有多条执行路径,则称为多线程程序
注意事项:
- 多进程的意义
- 对于单核计算机来讲,游戏进程和音乐进程不是同时进行的
因为CPU在某个时间点上只能做一件事情,计算机使在游戏进程和音乐进程间做着频繁的切换,且切换的速度很快,所以我们感觉不到游戏和音乐在同时进行,其实并不是同时进行 - 多进程的作用不是提高程序的执行效率,而是提高CPU的使用率
- 多线程的意义
- 多个线程不是并发执行的
因为多个线程共享同一个进程的资源(堆内存和方法区),但是栈内存是独立的,一个线程一个栈,所以他们仍然在抢CPU的资源进行,一个时间点上只能有一个线程执行,而且谁抢到,这个不一定,所以造成了线程运行的随机性 - 并行与并发的区别
- 并行是逻辑上同时发生,指在某一时间内同时运行多个程序
- 并发是物理上同时发生,指在某一时间点同时运行多个程序
☞☞☞☞☞☞☞☞☞☞☞☞☞☞☞第一种创建线程的方式☞☞☞☞☞☞☞☞☞☞☞☞☞☞☞☞ - 创建线程的两种方式
- 是将类声明为Thread的子类,该子类应重写Thread类的run方法。
- 然后,创建一个启动线程
代码实现:
package cn.edu360;
public class ThreadDemo {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();//使该线程开始执行,Java虚拟机调用该线程的tun方法
//thread.run();//相当于对象调用方法,还是在主线程执行
System.out.println("over");
}
}
class MyThread extends Thread{
@Override
public void run() {
try {
sleep(500);//让线程睡眠500毫秒,然后苏醒
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
注意事项:如果线程已经启动,再次启动会报错!
- public final viod setName(String name)设置线程的名字
- public final String getName()通过对象调用该方法返回线程的名字
- public static Thread currentThread()当前执行的线程
- Thread.currentThread()可以获得当前线程的对象
- public static void sleep(long millis)表示引用的当前线程睡眠多少时间
- public final void join()等待该线程终止(也就是:join方法通过实例调用所在的线程会等到该实例运行完毕之后再运行)
- public static void yield()暂停当前正在执行的线程对象,并执行其他线程
代码实现:
package cn.edu360;
public class ThreadDemo {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
try {
thread.join();//表示当前线程会等待thread线程执行完毕之后再执行主线程
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("over");
}
}
class MyThread extends Thread{
@Override
public void run() {
try {
sleep(500);//让线程睡眠500毫秒,然后苏醒
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
- public void interrupt()中断线程的阻塞状态(配合join方法使用)
代码实现:
package cn.edu360;
public class ThreadDemo {
public static void main(String[] args) {
//通过构造方法将主线程对象传给子线程
MyThread t = new MyThread(Thread.currentThread());
t.start();
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("over");
}
}
class MyThread extends Thread{
private Thread currentThread;
public MyThread(Thread currentThread) {
this.currentThread = currentThread;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i == 3){//当i等于3的时候中端主线程的阻塞状态
currentThread.interrupt();
}
System.out.println(i);
}
}
}
- public void interrupt()中断线程的阻塞状态(配合sleep方法使用)
代码实现:
package cn.edu360;
public class ThreadDemo {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
try {
t.interrupt();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("over");
}
}
class MyThread extends Thread{
@Override
public void run() {
try {
sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
线程的生命周期:
☞☞☞☞☞☞☞☞☞☞☞☞☞☞☞第二种创建线程的方式☞☞☞☞☞☞☞☞☞☞☞☞☞☞☞☞
- 是声明实现Runnable接口的类
- 该类然后实现run方法
- 然后可以分配该类的实例
在创建Thread时作为一个参数来传递并启动
代码实现:
package cn.edu360;
public class ThreadDemo {
public static void main(String[] args) {
MyRunnable task = new MyRunnable();
//多个线程可以执行一个Runnable任务
new Thread(task).start();
new Thread(task).start();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
案例展示:火车站售票
春节某火车站正在售票,假设还剩100张票,而它有3个售票窗口售票,请设计一个程序模拟该火车站售票;在真实生活中,售票时网络是不能实时传输的,总是存在延时的情况,所以,早出手一张票以后,需要一点时间的延迟,该实现接口的方式实现买票程序,每次卖票延迟100毫秒
代码实现:
ThreadTest.java
package cn.edu360;
public class ThreadTest {
public static void main(String[] args) {
//继承Thread类
// TicketThread t = new TicketThread("窗口1");
// t.start();
// TicketThread t2 = new TicketThread("窗口2");
// t2.start();
// TicketThread t3 = new TicketThread("窗口3");
// t3.start();
//实现Runnable接口
TicketRunnable task = new TicketRunnable();
new Thread(task, "窗口一").start();
new Thread(task, "窗口二").start();
new Thread(task, "窗口三").start();
}
}
TicketThread.java
package cn.edu360;
public class TicketThread extends Thread{
private static int ticket = 100;
public TicketThread(String name) {
super(name);
}
@Override
public void run() {
while(ticket > 0){
System.out.println(getName()+"正在出售第"+ticket+"张票");
ticket--;
}
}
}
TicketRunnable.java
package cn.edu360;
public class TicketRunnable implements Runnable {
private int ticket = 100;
@Override
public void run() {
while(ticket > 0){
System.out.println(Thread.currentThread().getName()+"正在出售第"+ticket+"张票");
ticket--;
}
}
}
上述代码会出现线程同步安全问题;
- 什么情况下会出现多线程安全问题
- 多线程环境下
- 存在共享数据
- 共享数据被多条语句使用
- 怎样解决线程安全问题了?
- 就是让程序不存在线程安全问题,让操作共享数据的代码在任意时间只能被同一个线程所操作
- 可以使用同步代码块实现
锁对象:锁对象可以是任意对象,多个线程使用的必须是同一把锁,也就是说必须是同一个对象,或者说锁对象必须唯一
代码修改:
TicketRunnable.java
package cn.edu360;
public class TicketRunnable implements Runnable {
private int ticket = 100;
@Override
public void run() {
synchronized (Object.class) {
while(ticket > 0){
System.out.println(Thread.currentThread().getName()+"正在出售第"+ticket+"张票");
ticket--;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
同步代码块的优缺点:
- 优点:解决了线程安全问题
- 缺点:没出运行的时候都要检查锁对象是否被释放
同步方法:
就是在方法修饰符上加synchronized,将整个方法的代码都锁起来
- 非静态同步方法的锁对象又是什么了?
this - 静态方法的锁对象又是什么了?
就是当前类的字节码文件对象 - 锁对象
锁对象可以是任意对象,多个线程使用的必须是同一把锁,也就是说必须是同一个对象,或者说锁对象必须唯一
注意事项:
- 什么时候使用同步代码块,什么时候使用同步方法?
- 当方法里面只有一部分代码存在安全问题时,使用同步代码块
- 当方法里面所有的代码都存在安全问题时且当前锁对象可以this时使用同步方法
代码修改:
TicketRunnable.java
package cn.edu360;
public class TicketRunnable implements Runnable {
private int ticket = 100;
@Override
public synchronized void run() {
while(ticket > 0){
System.out.println(Thread.currentThread().getName()+"正在出售第"+ticket+"张票");
ticket--;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
锁对象什么时候释放:
- 同步代码执行完毕
- 线程进入等待状态时
- 线程停止
线程等待与线程唤醒:
- public final void wait()使线程等待
- public final void notify()随机唤醒等待的其中一个线程
- public final void notifyAll()唤醒所有在等待的线程,每个线程的唤醒顺序也是随机的
- public void interrupt()可以中断等待的线程状态
注意上述方法必须在同步代码里面通过锁对象调用
注意事项:
- 为什么wait,notify,notifyAll方法是定义在Object类中
- 因为锁对象可以是任意对象,又因为wait,notify,notifyAll必须通过锁对象调用;
- 所以任意对象都可以调用的方法应该定义在Object类中
锁:
- JDK1.5之后将锁封装成了一个接口Lock,里面提供了上锁和解锁的方法
- public ReentrantLock()
- public void lock()
- public void unlock()
代码修改:
TicketRunnable.java
package cn.edu360;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TicketRunnable implements Runnable {
private int ticket = 100;
private Lock lock = new ReentrantLock();
@Override
public void run() {
while(ticket > 0){
lock.lock();//上锁
System.out.println(Thread.currentThread().getName()+"正在出售第"+ticket+"张票");
ticket--;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.unlock();//解锁
}
}
}
死锁:
A线程:在等待B线程释放锁
B线程:在等待A线程释放锁
注:尽量尽可能的多个线程使用同一把锁
代码实现:
package cn.edu360;
public class DieLock {
public static void main(String[] args) {
MyThread myThread = new MyThread(true);
myThread.start();
MyThread myThread2 = new MyThread(false);
myThread2.start();
}
}
class MyLock{
public static Object lockA = new Object();
public static Object lockB = new Object();
}
class MyThread extends Thread{
private boolean flag;
public MyThread(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if(flag){
synchronized (MyLock.lockA) {
System.out.println("lockA");
synchronized (MyLock.lockB) {
System.out.println("lockB");
}
}
}else{
synchronized (MyLock.lockB) {
System.out.println("lockB");
synchronized (MyLock.lockA) {
System.out.println("lockA");
}
}
}
}
}
线程池:
我们之前创建使用线程,使用完了就销毁了,因为创建线程是很消耗资源的,所以Java就提供了线程池这个技术来提高线程的使用效率;
可以通过Executors这个工厂类就可以返回我们想要的线程池,里面有不同种类的线程池,我们也可以根据自己的需求使用ThreadPoolExecutor自定义线程池
创建线程池的三种方式:
- public static ExecutorService newCachedThreadPool()创建一个线程存活时间为60秒的线程池
- public static ExecutorService newSingleThreadExecutor()创建一个线程池,里面只有一个线程,在某个线程被显示的关闭之前,池中的线程将一直存在
- public static ExecutorService newFixedThreadPool(int nThreads)创建一个指定线程数的线程池。在某个线程被显示的关闭之前,池中的线程将一直存在
代码实现:
package cn.edu360;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class DieLock {
public static void main(String[] args) throws Exception {
//第一种方式
ExecutorService threadPool = Executors.newCachedThreadPool();
threadPool.execute(new MyThread());
//第二种方式
ExecutorService threadExecutor = Executors.newSingleThreadExecutor();
threadExecutor.execute(new MyThread());
//第三种方式
ExecutorService threadPool2 = Executors.newFixedThreadPool(1);
threadPool2.execute(new MyThread());
/**
* 下述方式和join的含义相类似
*/
Future<String> future = threadPool.submit(new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(5000);
//要执行的代码
return "执行以后的结果";
}
});
String result = future.get();
System.out.println(result);
System.out.println("over");
}
}
class MyThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
定时器:
定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式执行。在Java中,可以通过TImer和TimerTask类来实现定义
- Timer:一种工具,线程用其安排以后在后台线程中执行的任务。可安排任务执行一次,或者定期重复执行
- public Timer()创建一个新的定时器
- public void schedule(TimerTask task,long delay)安排在指定延迟后执行指定的任务;task-----所要安排的任务;delay-----执行任务期的延迟时间,单位是毫秒
- public void schedule(TimerTask task,long delay,long period)安排在指定延迟后执行指定的任务;task-----所要安排的任务;delay-----执行任务期的延迟时间,单位是毫秒;period-----执行各后续任务之间的时间间隔,单位是毫秒
代码实现:
package cn.edu360;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
public class DieLock {
public static void main(String[] args) throws Exception {
Timer timer = new Timer();
myTimerTask timerTask = new myTimerTask();
//timer.schedule(timerTask, 3000);
//timer.schedule(timerTask, 3000, 1000);
timer.schedule(timerTask, 1000, 1000);
}
}
class myTimerTask extends TimerTask{
@Override
public void run() {
//System.out.println("我要3秒后执行");
//制作简单的闹钟
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String value = format.format(new Date());
System.out.println(value);
}
}
☞☞☞☞☞☞☞☞☞☞☞☞☞☞☞☞☞☞☞☞☞☞☞线程面试题汇总:☞☞☞☞☞☞☞☞☞☞☞☞☞☞☞☞☞☞☞☞
- 多线程有几种实现方案,分别是哪几种
- 继承Thread类,重写run方法,然后创建该子类的实例,然后调用start方法
- 实现Runnable接口,实现run方法,然后将该子类的实例作为参数传递给Thread,然后调用start方法
- 同步有几种方式,分别是什么?
- 锁对象:必须保证唯一或者说多个线程需要使用同一把锁
- 同步代码块:同步代码块的锁对象可以是任意对象,只要保证多个线程使用的锁对象一致就可以了
synchronized(锁对象){} - 同步方法:锁对象是this;将synchronized放在方法的修饰位置上
- 静态同步方法的锁对象是当前类的字节码文件对象
- 使用lock子类对象,创建ReentrantLock子类对象然后使用lock和unLock分别加锁和解锁
- 启动一个线程是run()还是start()?它们有什么区别?
- 使用start,start方法会启动一个线程,然后JVM在通过线程调用run方法
- 直接调用run方法就相当于一个对象调用方法,最终还是运行在主线程中
- sleep()和wait()方法的区别
- sleep():是睡眠指定的时间,等到指定时间到了就苏醒
- wait():只能在同步代码中通过锁对象调用,必须通过锁对象唤醒
- 线程的生命周期(如上述图所画)