一、线程简介
1、进程和线程
- 程序: 开发出来的代码称之为程序。程序就是一堆代码,是一个静态的概念。(车间)
- 进程(Process): 程序运行起来我们称为进程。进程是程序的一次执行,是一个动态的概念。进程有生命周期,会随着程序的终止而销毁。(运作的车间)
- 线程(Thread): 线程是进程的运作单位。一个进程包含多个线程。(车间的任意一条流水线)
- 注意:很多多线程都是模拟出来的,真正的多线程是指有多个CPU,即多核,如服务器,如果是模拟出来的多线程,即一个CPU的情况下,在同一个时间点,CPU只能执行一个代码,因为切换的很快,所以就有同时执行的错觉。
2、并发、并行、串行
- 并发: 同一个对象被多个线程同时操作。(这是一种假并行。即一个CPU的情况下,在同一个时间点,CPU只能执行一个代码,因为切换的很快,所以就有同时执行的错觉)。
- 并行: 多个线程同时执行。并行必须有多核才能实现,否则只是并发(假并行)
- 串行: 多个线程执行完一个再执行下一个。
3、线程生命周期的五种状态
- 新建: 线程已经创建,但并未调用start()方法启动。
- 就绪: 线程启动start()后,等待CPU分配资源。
- 运行: 当就绪的线程获取CPU资源时,便进入运行状态,run()方法定义了线程池的操作和功能。
- 阻塞: 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态。
- 死亡: 线程完成他的全部工作或线程被提前强制性的中止或异常导致结束。
二、线程实现
1、继承Thread类,重写run方法
public class WindowsThread {
public static void main(String[] args) {
WindowThd w1 = new WindowThd();
WindowThd w2 = new WindowThd();
WindowThd w3 = new WindowThd();
w1.setName("线程1");
w2.setName("线程2");
w3.setName("线程3");
w1.start();
w2.start();
w3.start();
}
}
class WindowThd extends Thread{
private static int num=100;
public void run(){
while(true){
//同步代码块
// synchronized (WindowThd.class){
// if(num>0){
// System.out.println("---"+Thread.currentThread().getName()+"出售票:"+num+"---");
// num--;
// }else{
// break;
// }
// }
show();
}
}
//
public synchronized void show(){//同步监视器WindowThd.class
if(num>0){
System.out.println("---"+Thread.currentThread().getName()+"出售票:"+num+"---");
num--;
}
}
}
2、实现Runnable接口,重写run方法
public class WindowsRunnable {
public static void main(String[] args) {
WindowRun w = new WindowRun();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3= new Thread(w);
t1.setName("线程1");
t2.setName("线程2");
t3.setName("线程3");
t1.start();
t2.start();
t3.start();
}
}
class WindowRun implements Runnable{
private int num=100;
public void run(){
while(true){
synchronized (this){
if(num>0){
System.out.println("---"+Thread.currentThread().getName()+"出售票:"+num+"---");
num--;
}else{
break;
}
}
show();
}
}
public synchronized void show(){//同步监视器this
if(num>0){
System.out.println("---"+Thread.currentThread().getName()+"出售票:"+num+"---");
num--;
}
}
}
3、实现Callnable接口,重写run方法
public class WindowsCallnable {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<String> w = new WindowCall();
Callable<String> callA = new WindowCall();
Callable<String> callB = new WindowCall();
Callable<String> callC = new WindowCall();
FutureTask<String> futureA = new FutureTask<String>(callA);
FutureTask<String> futureB = new FutureTask<String>(callB);
FutureTask<String> futureC = new FutureTask<String>(callC);
new Thread(futureA).start();
new Thread(futureB).start();
new Thread(futureC).start();
System.out.println("A执行返回结果:" + futureA.get());
System.out.println("B执行返回结果:" + futureB.get());
System.out.println("C执行返回结果:" + futureC.get());
}
}
class WindowCall implements Callable<String> {
@Override
public String call() throws Exception {
for(int i = 0 ; i < 100; i++) {
System.out.println("线程运行, x = " + i);
}
return "执行完毕"; // 返回值
}
}
三、线程常用方法
Thread类api方法
1. start(): 启动当前线程:调用当前线程的run()
2. run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
3. currentThread(): 静态方法,返回执行当前代码的线程
4. getName(): 获取当前线程的名字
5. setName(): 设置当前线程的名字
6. yieId(): 释放当前cpu的执行权
7. join(): 在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态
8. stop(): 已过时。当执行此方法时,强制结束当前线程
9. sleep(): 让当前线程“睡眠”指定的millitime毫秒。在指定的时间内,当前线程是阻塞状态
10. isAlive(): 判断当前线程是否存活
通讯方法
1. wait():
2. notify():
1. notifyAll():
四、多线程应用
1、线程安全性问题
例子:创建三个窗口卖票,总票数为100张,使用实现Runnable接口的方式
1、问题:卖票过程中,出现了重票、错票 --->出现了线程的安全问题
2、问题出现原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票,同时操作num
会发生重票和错票的情况。
3、如何解决:当一个线程a在操作num的时候,其他线程不能参与进来。直到线程操作完成num时,其他线程才可以
开始操作num。这种情况及时线程a出现了阻塞,也不能被改变。
4、线程安全性问题
一、同步代码块
synchronized(同步监视器){
//需要被同步的代码
}
说明:
1、操作共享数据的代码,即需要被同步的代码。 --->不能包含多了,也不能包含少了
2、共享数据:多个线程共同操作的变量。比如num就是共享数据
3、同步监视器:俗称,锁。任意一个类的对象,都可以充当锁
要求:多个线程必须共用一把锁
补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器
在继承Thread类创建多线程的方式中,我们可以用类名.class充当同步监视器
二、同步方法
//继承Thread类
public static synchronized void show(){//同步监视器WindowThd.class
//需要被同步的代码
}
//实现Runnable接口
public synchronized void show(){//同步监视器this
//需要被同步的代码
}
在需要被同步的方法上加synchronized
注意:Thread必须用static修饰方法
1)继承Thread类的同步方法必须加static修饰方法,同步监视器指本类.class
2)实现Runnable接口的同步方法不需要加static,同步监视器指本类this
备注:
同步的方式,解决了线程的安全问题。---好处
操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。---局限性
三、lock同步代码块
解决线程安全问题的方式三:Lock锁 ---Jdk5.0新增
1、synchronized与lock的对比
1)lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
2)lock只有同步代码块锁,synchronized有同步代码块锁和同步方法锁
3)使用lock锁,Jvm将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
优先使用顺序
lock-->同步代码块(已经进入了方法体,分配了相应资源)-->同步方法(在方法体之外)
2、面试题:synchronized与Lock的异同?
相同:二者都可以解决线程安全问题
不同:synchronized机制在执行完全相应的同步代码以后,自动的释放同步监视器
lock需要手动的启动同步lock()方法,同时结束同步也需要手动的实现unlock()方法
3、面试题:如何解决线程安全问题?有几种方式
两种方式
synchronized:同步代码块,和同步方法体
lock:同步代码块
class WindowLk implements Runnable{
private static int num=100;
//1、实例化ReentrantLock true是指是否交替执行
private ReentrantLock lock = new ReentrantLock(true);
public void run(){
while(true){
try{
//2、调用锁定方法lock()
lock.lock();
if(num>0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("---"+Thread.currentThread().getName()+"出售票:"+num+"---");
num--;
}else{
break;
}
}finally{
//3.调用解锁方法:unlock()
lock.unlock();
}
}
}
}
2、线程死锁
1、死锁的理解:
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要同步的同步资源,就形成了线程的死锁
2、说明:
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
- 我们使用同步时,要避免出现死锁
五、多线程常见面试题
1、比较创建线程的俩种方式(Thread和Runnable)
开发时: 优先选择Runnable接口的方式
原因:
- 实现的方式没有类的单继承方式的局限性
- 实现的方式更适合来处理多线程的共享数据的情况
Treand共享线程类的全局变量必须加static
Runnable共享线程类的全局变量不需要加static
联系: public class Thread implements Runnable
相同点: 俩种方式都需要重写run(),将线程要执行的逻辑声明再run()中
2、说说你对IDEA中Project和Module的理解
1、Project:相当于eclipse中的workspace
2、Module:相当于eclipse中的project