一.什么是程序、进程、线程?
程序:就是用编程语言编写的一段静态的代码。
进程:一个正在运行的应用程序
线程:一个程序内部的一条执行路径
线程、进程根本区别:
① 进程是资源分配的基本单位,而线程是处理器任务调度和执行的基本单位;
② 一个进程可以由多个进程组成,但是一个线程只能属于一个进程;
③ 每个线程拥有自己独立的栈、程序计数器(pc),多个线程共享一个进程的结构和资源:堆、方法区
并行:多个cpu同时执行多个任务
并发:一个cpu(采用时间片)同时执行多个任务,如:秒杀
二.线程的创建方式
1.继承Thread类,重run()方法
① 创建子类继承Thread类
② 子类重写run()方法(线程需要实现的逻辑功能)
③ 子类创建对象
④ 使用对象调用start()方法(启动当前线程、调用当前线程的run()方法)
注意:
启动一个线程,必须调用start()方法,不要使用对象调用重写后的run()方法,此时就相当于在main线程中使用“对象.方法”调用方法,无法体现多线程的功能。
public class ThreadTest {
public static void main(String[] args) {
//Thread.currentThread().getName():获取当前运行的线程名
System.out.println(Thread.currentThread().getName());
//3.创建子类对象
MyThread myThread = new MyThread();
//4.调用start()方法
myThread.start();
// myThread.start(); //java.lang.IllegalThreadStateException
}
}
//1.创建子类继承Thread类
class MyThread extends Thread{
@Override
//2.重写run()方法
public void run() {
for(int i=0;i<100;i++){
if(i%2!=0){
System.out.println(Thread.currentThread().getName()+"-i :" +i);
}
}
}
}
说明:
① Thread中常用的方法:
/**
* @author wds
* @date 2021-11-15-17:30
*/
public class ThreadTest01 {
public static void main(String[] args) {
Mythread01 mythread01 = new Mythread01();
Mythread01 mythread02 = new Mythread01();
mythread01.run(); //1.run():调用当前线程的run()方法
System.out.println("main函数所在的线程是:"+Thread.currentThread().getName());
mythread01.setName("线程1"); //2.setName(String name):给线程命名
mythread02.setName("线程2");
mythread01.start();
mythread02.start();
System.out.println("mythread01线程是否存活:"+mythread01.isAlive());
}
}
class Mythread01 extends Thread{
@Override
public void run() { //3.run():声明线程需要实现的功能
for(int i=0;i<5;i++){
if(i%2==0){
try {
sleep(100); //4.sleep(long mill)使线程修眠,暂时放弃对cpu的控制
} catch (InterruptedException e) {
e.printStackTrace();
} //5.currentThread():获取当前代码的线程
yield(); //6.yield():释放当前的cpu执行权
//7.getName():当前的线程名
System.out.println("run所在线程名"+Thread.currentThread().getName()+" i:"+i);
}
}
}
}
② 线程的调度:
策略:时间片策略、抢占式调度
Java调度策略:同优先级先来先服务时间片策略,高优先级抢占cpu;
MAX_PRIORITY:10
MIN_PROIORITY:1
NORM_PRIORITY:5(默认优先级)
getPriority():获取线程的优先级
setPriority(int p):设置线程的优先级
说明:高优先级的线程会抢占低优先级的线程cpu的执行权,并不是一定会执行高优先级的线程。
public class ThreadTest01 {
public static void main(String[] args) {
Thread1 thread1 = new Thread1();
Thread t1 = new Thread(thread1,"线程1");
Thread t2 = new Thread(thread1,"线程2");
t1.setPriority(8); //设置线程t1的优先级为8
t1.start();
t2.start();
}
}
class Thread1 implements Runnable{
public static int number = 1;
@Override
public void run() {
while(true){
synchronized (this) {
if (number < 100) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(number%2==0){
System.out.println(Thread.currentThread().getName() + "输出:" + number
+ ",优先级是" + Thread.currentThread().getPriority());
}
number++;
}
}
}
}
}
③ 线程的分类
根据JVM何时离开,分为:
守护线程:为用户进程服务,使用start()调用。如:Java垃圾回收
用户线程:
若JVM都是守护进程,JVM将推出。
④ 用户进程—>守护进程:thread.setDaemon(true)
2.实现Runnable接口,重写run()方法
① 创建一个类实现Runnable接口
② 重写run()方法
③ 创建实现类对象
④ 将实现类对象作为参数传入Thread构造器中,创建构造器的对象
⑤ 通过Thread对象调用start()
public class ThreadTest02 {
public static void main(String[] args) {
//3.创建实现类对象
MyThread02 myThread02 = new MyThread02();
//4.将实现类对象作为参数传给Thread构造器,创建Thread对象
Thread thread1 = new Thread(myThread02,"线程1");
//5.通过Thread对象调用start()
thread1.start();
Thread thread2 = new Thread(myThread02, "线程2");
thread2.start();
}
}
//1.创建类实现Runnable接口
class MyThread02 implements Runnable{
@Override
//2.重写run()方法
public void run() {
int i = 0;
while(i++<3){
System.out.println(Thread.currentThread().getName()+"的run方法在运行...");
}
}
}
3.实现Callable接口,重写call()方法
① 创建类实现Callable接口
② 重写实现call()方法
③ 创建实现类的对象
④ 将实现类的对象传递给FutureTask构造器,创建FutureTask的对象
⑤ 将FutureTask的实现类作为参数传递给Thread创建Thread类的对象,调用start()方法
⑥ 获取Callable接口的返回值
public class ThreadTest01 {
public static void main(String[] args) {
//3.创建Callable接口实现类的对象
Thread2 thread2 = new Thread2();
//4.将实现类的对象传递给FutureTask构造器,创建FutureTask的对象
FutureTask futureTask = new FutureTask(thread2);
//5.将FutureTask的实现类作为参数传递给Thread创建Thread类的对象,调用start()方法
new Thread(futureTask).start();
try {
//6.获取Callable接口的返回值
//get()返回call()方法重写后的返回值
Object sum = futureTask.get();
System.out.println("1-100求和为:"+sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
//1.定义一个类实现Callable接口
class Thread2 implements Callable{
int sum = 0;
@Override
//2.实现call()方法,将此线程实现的操作声明在方法中
public Object call() throws Exception {
for(int i = 1;i<=100;i++){
sum += i;
System.out.println(sum);
}
return sum;
}
}
实现Runnable、Callable接口异同:
① Runnable接口重写run()方法,没有返回值,无法抛出异常;
② Callable接口重写call()方法,可以有返回值,可以抛出异常;
③ 推荐使用Callable接口方式、线程池的方式实现多线程。
4.使用线程池
① 定义一个接口的实现类,实现Runnable接口或Callable接口
② 提供指定数量的线程池
③ 创建接口的实现类对象
④ 将实现类对象提交到线程池进行管理
⑤ 关闭线程池
//1.定义一个接口的实现类,实现Runnable接口或Callable接口
class Thread11 implements Runnable{
@Override
public void run() { //线程需要实现的操作
for(int i = 0;i<100;i++){
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//2.提供指定数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//管理线程池强制类型转换为ThreadPoolExecutor,利用service01管理线程池
ThreadPoolExecutor service01 = (ThreadPoolExecutor)service;
System.out.println(service.getClass());
//3.创建接口的实现类对象
Thread11 thread1 = new Thread11();
Thread11 thread2 = new Thread11();
//4.将实现类对象提交到线程池进行管理
//service.submit(); //Callable接口实现
service.execute(thread1); //Runnable接口实现
service.execute(thread2);
//5.关闭线程池
service.shutdown();
}
}
线程池的好处
① 提高响应速度(减少了创建新线程的时间)
② 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
③便于线程管理
三.线程的生命周期
线程的五大状态:创建、就绪、运行、阻塞、死亡。
四.线程通信
线程通信的三个方法:
① wait():线程进入阻塞,释放同步监视器
② notify():唤醒被wait()的一个线程,当有多个被wait()时,唤醒优先级高的。
③ nitifyAll():唤醒被wait()的多个线程
注意:
这三个方法必须使用在同步方法或同步代码块中,调用者必须是同步代码块或同步方法的监视器。
线程通信例子:两个线程交替打印输出1-20的数字
/**
* 线程通信例子:两个线程交替打印输出1-20的数字
* @author wds
* @date 2021-11-16-16:03
*/
public class ThreadTest {
public static void main(String[] args) {
Thread1 thread1 = new Thread1();
Thread a = new Thread(thread1, "线程a");
Thread b = new Thread(thread1, "线程b");
a.start();
b.start();
}
}
class Thread1 implements Runnable {
private static int number = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
if (number <= 20) {
notify();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "打印:" + number);
number++;
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
五.线程安全问题
线程安全问题的处理:
例题:模拟三个窗口共同售100张票(附源码)
1.同步代码块:
语法格式:
synchronized(监视器){
//需要同步的代码
}
监视器又叫锁,任何一个对象都可以作为锁,多个线程共享的锁唯一即可
① 实现Runnable接口的方式中,可以使用this关键字或对象名,也可以使用“类名.class”
② 继承Thread方式中,慎用this,可以使用“类名.class”。
好处:解决了同步问题,实现了线程安全;
不足:操作同步代码块内只有一个线程执行,相当于单线程,其他等待,效率低。
2.同步方法
语法格式:
[修饰符] synchronized 返回值类型 方法名(参数列表){
//需要同步的代码
}
同步方法说明
① 同步方法仍然设计到同步监视器,只是不需要我们显式声明;
② 非静态的同步方法,同步监视器是this;静态的同步方法,监视器是类本身;
3.同步锁lock
优先使用顺序:
Lock—>同步代码块(已经进入了方法体,分配了相应资源)—>同步方法(在方法体之外)
六.死锁问题
所谓死锁,是指不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的向步资源,就形成了线程的死锁。
死锁面试题
public class ThreadTest {
public static void main(String[] args) {
StringBuffer str1 = new StringBuffer();
StringBuffer str2 = new StringBuffer();
new Thread(){ //继承Thread类方式的匿名对象
@Override
public void run() {
synchronized (str1){
str1.append(1);
str2.append("a");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (str2){
str1.append("b");
str2.append(2);
System.out.println(str1);
System.out.println(str2);
}
}
}
}.start();
new Thread(new Runnable() { //实现Runnable接口方式的匿名对象
@Override
public void run() {
synchronized (str2){
str1.append(3);
str2.append("a");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (str1){
str1.append("b");
str2.append(4);
System.out.println(str1);
System.out.println(str2);
}
}
}
}).start();
}
}
死锁线程不报错,不终止,必须避免死锁
面试题:
1.Lock和synchronized有什么区别?
相同:二者都可以解决线程安全问题
不同:
Lock是显式锁(手动开启和关闭锁);synchronized是隐式锁,出了作用域会自动释放
Lock只有代码块锁,synchronized有代码块锁和同步方法锁
2.处理线程安全有几种方式?
三种:同步方法,同步代码块、lock同步锁
3.sleep()和wait()方法的异同?
相同:一旦调用方法,都会使得线程进入阻塞状态
不同:
① 声明位置不同。sleep()声明在Thread中,wait()声明在Object中;
② sleep()在任何场景下都可以调用,wait()在同步代码块或同步方法中;
③ 若都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。