Java
day19_2023.9.20
多线程
程序、进程、线程
程序:程序其实是指令和数据的有序集合,本身没有特殊含义,是一个静态概念
进程:进程是执行程序的一次执行过程,它是一个动态的概念,本质上其实是系统的资源分配单位,当我们运行一个程序后,这个程序的进程就会启动。
线程:线程其实是进程中的CPU调度和执行的单位,进程中可以包含若干个线程,一个进程最少有一个线程
在同一个进程中,可以执行多个任务,而每个任务就可以看成是一个线程
比如: 放音乐的时候,可以一边放音乐,一边点击菜单,或者一边下载其他音乐
总结 :
进程其实就是指运行中的程序,特点 :动态、独立、并发
线程是进程内部的一个执行单元,是程序中某一个功能的单一顺序控制流程
面试题:进程和线程的区别
1,根本区别 : 进程是资源的分配单位 ,线程是资源的执行单位
2,开销:
进程具有独立的代码和数据空间,进程间的切换开销较大
线程可以看做是轻量的进程,每个线程有独立的运行区域,线程之间开销小
3,所处环境:
进程是运行在操作系统中的多个任务
线程是在同一个程序中,有多个顺序流同时执行
4,内存分配 :
进程在运行的时候,系统会为每个进程分配不同的内存区域
线程的内存是由CPU分配的,
5,包含关系:
进程包含线程
没有线程的进程,可以看做是单线程的,如果一个进程有多个线程,执行过程就是多条线的
Java中的线程
主线程
Java中main()方法就是主线程的入口
其他的子线程运行,必须在main()线程中,所以,main()方法必须是最后完成执行的,因为它要执行各种关闭动作。
即使在程序运行的时候,没有自定线程,程序中也包含多个线程,比如 gc程序
将来,在一个程序中,如果开辟了多个线程,那么线程的运行由CPU(调度器)来完成调度,先后顺序不能人为干预
将来有多个线程的时候,如果对同一份资源操作,会存在资源抢夺的问题(线程不安全问题),需要加入并发控制。
Java中线程的创建和启动
Java中创建线程的方式 一共有四种:
1,继承Thread类
2,实现Runnable接口
3,实现Callable接口
4,使用线程池Executors工具类创建线程池
继承Thread类实现多线程
构造方法
Thread()
分配一个新的 Thread对象。
Thread(Runnable target)
分配一个新的 Thread对象。
Thread(Runnable target, String name)
分配一个新的 Thread对象。
Thread(String name)
分配一个新的 Thread对象。
继承Thread类创建多线程步骤
1,自定义一个类,继承Thread,并且在类中,重写run方法,run方法中,存放的是你需要实现的具体的逻辑代码。
2,创建自定义的线程子类对象
3,通过线程子类对象,调用start()方法,来启动子线程
为什么是调用start()方法 ?
线程调用start()方法之后,表示该线程已经准备好被执行了,然后再有JVM调用run()方法,
如果直接在main()方法中,调用run()方法,那么run()方法就被当做普通方法执行了
public class MyThread extends Thread{
@Override
public void run() {
//System.out.println("子线程--" + Thread.currentThread().getName() + "--在运行");
for (int i = 0; i < 5; i++) {
System.out.println("线程--" + Thread.currentThread().getName() + "--在运行");
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
/* //获取当前执行的线程对象
Thread thread = Thread.currentThread();
System.out.println(thread.isAlive()); //true
System.out.println("当前运行的线程是:" + thread.getName());
//给当前线程重新设置个名字
thread.setName("主线程");
System.out.println("当前运行的线程是:" + thread.getName());
//创建子线程对象,通过start()方法启动线程
//MyThread myThread = new MyThread();
//myThread.start();
new Thread(new MyThread(),"创建的子线程1").start();
new Thread(new MyThread()).start();*/
for (int i = 0; i < 5; i++) {
System.out.println("线程--" + Thread.currentThread().getName() + "--在运行");
}
MyThread myThread = new MyThread();
//如果直接调用run()方法,其实这个run()方法被当做普通方法调用
//这个时候,也是main线程在执行
//myThread.run();
myThread.start();
}
}
继承Thread创建多线程,并创建2个子线程,一个线程叫 :小明,一个线程叫:小红,
分别让这两个线程循环5次,输出以下内容: 当前线程名 : xxx ,正在被调用
public class ThreadDemo1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("当前的线程:" + Thread.currentThread().getName()
+"正在被调用");
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class TestDemo1 {
public static void main(String[] args) {
new Thread(new ThreadDemo1(),"小明").start();
new Thread(new ThreadDemo1(),"小红").start();
}
}
实现Runnable接口创建线程
实现步骤:
1,自定义一个类,实现Runnable接口,重写run()方法
2,创建实现类的对象,并以这个对象,作为new Thread()的参数,构造出Thread类的对象
3,通过Thread类的对象调用start()方法
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "--" + i);
}
}
}
public class RunnableTest {
public static void main(String[] args) {
//直接创建出来的Runnable接口对象,不能直接调用start()方法
MyRunnable myRunnable = new MyRunnable();
//myRunnable.
//先创建Thread对象,将线程对象传进去
new Thread(myRunnable).start();
//创建Thread对象的时候,直接存入Runnable对象,并取名
new Thread(new MyRunnable(),"线程2").start();
}
}
使用匿名内部类实现
public class RunnableDemo01 {
public static void main(String[] args) {
//通过匿名内部类的方式实现多线程
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "--" + i);
}
}
}).start();
}
}
继承Thread和实现Runnable接口的区别
1,继承Thread类
将来就不能继承其他类了
编写简单,可以直接操作线程对象,无须再通过Thread类去调用其他方法
2,实现Runnable接口
将来还可以继承其他类
可以实现对象的数据共享(会存在并发问题)
将来使用Runnable接口的方式实现多线程比较常用
初识线程安全问题
public class TicketRunnable implements Runnable {
private int ticketNum = 10;
@Override
public void run() {
while (true){
if (ticketNum <=0 ){
break;
}
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+"---->抢到了第"+ticketNum-- +"张票");
}
}
public static void main(String[] args) {
//这里操作的不是相同的对象,所以他们输出的是30条内容
//new Thread(new TicketRunnable(),"小明").start();
//new Thread(new TicketRunnable(),"小红").start();
//new Thread(new TicketRunnable(),"黄牛").start();
//在外面创建对象,将对象传入new Thread中,创建多个子线程
//这时候,多个子线程就会共享这个对象中的数据
TicketRunnable ticketRunnable = new TicketRunnable();
new Thread(ticketRunnable,"小明").start();
new Thread(ticketRunnable,"小红").start();
new Thread(ticketRunnable,"黄牛").start();
}
}
线程的生命周期
**新生状态 :**使用new关键字建立一个线程对象,该线程对象就处于新生状态
处于新生状态的线程,拥有自己的内存空间,通过调用start方法进入就绪状态
就绪状态 : 新建的线程,通过调用start方法,进入就绪状态
就绪状态表示,线程可运行了,但是还没被分配到cpu,处于就绪队列,等待系统分配CPU
当系统选定一个等待的线程,就会从就绪进入执行状态,这个动作也称为cpu调度
**运行状态:**运行状态的线程,获得了cpu时间片,执行自己的run()方法中的代码,
直到完成任务死亡或者等资源而阻塞
阻塞状态:运行状态的线程,在某些情况下,会进入阻塞状态
阻塞分为三种 :
1,等待阻塞 : 调用wait方法会出现
2,同步阻塞: 调用方法的时候,方法上加了synchronized关键字
3,其他阻塞 : 调用sleep()方法、join()方法、或者io阻塞等,会发生
死亡状态: 线程的生命周期最后一个阶段,死亡的原因 :
1,所有内容正常执行完毕了,线程会死亡
2,线程执行被强制终止
3,线程抛出异常未被捕获
public class RunnableDemo implements Runnable {
@Override
public void run() {
System.out.println("线程执行了");
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程在经过短暂的休眠后,重新执行了");
}
}
public class RunnableTest {
public static void main(String[] args) {
RunnableDemo runnableDemo = new RunnableDemo();
Thread thread = new Thread(runnableDemo);
System.out.println("线程被创建");
thread.start();
System.out.println("线程准备就绪");
}
}
线程调度相关方法
void setPriority(int newPriority) 更改此线程的优先级。
public class RunnableDemo implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "--" + i);
}
}
}
public class RunnableTest {
public static void main(String[] args) {
RunnableDemo runnableDemo = new RunnableDemo();
//Thread thread1 = new Thread(new RunnableDemo(), "线程1");
//Thread thread2 = new Thread(new RunnableDemo(), "线程2");
Thread thread1 = new Thread(runnableDemo, "线程1");
Thread thread2 = new Thread(runnableDemo, "线程2");
thread1.setPriority(Thread.MAX_PRIORITY); //设置线程1 的优先级
thread2.setPriority(Thread.MIN_PRIORITY);
//设置优先级并不代表,优先级高的会先执行完
thread1.start();
thread2.start();
}
}
void join() 等待这个线程死亡。
public class JoinTest {
public static void main(String[] args) throws InterruptedException {
//创建子线程并执行
Thread t = new Thread(new RunnableDemo(),"子线程");
t.start();
//主线程main执行的内容
for (int i = 0; i < 10; i++) {
if (i == 5){
t.join(); //使用join方法,完成插队的操作
}
System.out.println(Thread.currentThread().getName() + "--" + i);
}
}
}
static void yield() 对调度程序的一个暗示,即当前线程愿意出让当前使用的处理器。
public class JoinTest {
public static void main(String[] args) throws InterruptedException {
//创建子线程并执行
Thread t = new Thread(new RunnableDemo(),"子线程");
t.start();
//主线程main执行的内容
for (int i = 0; i < 10; i++) {
if (i == 5){
t.yield(); //线程礼让
//执行yield()方法,不代表后面的执行全部让给其他线程
//只是会出让下一次执行机会,执行完后,cpu继续执行调度
}
System.out.println(Thread.currentThread().getName() + "--" + i);
}
}
}
void setDaemon(boolean on) 将此线程标记为 daemon线程或用户线程。 当程序中正在运行的线程都是守护线程,那么JVM将退出(程序结束)
这个方法,必须要在线程启动前调用
常用的JVM垃圾回收线程、内存管理线程,都是守护线程
public class DaemonDemo implements Runnable{
@Override
public void run() {
while (true){
System.out.println("守护线程在运行");
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class DaemonTest {
public static void main(String[] args) {
System.out.println("程序开始");
DaemonDemo daemonDemo = new DaemonDemo();
Thread t = new Thread(daemonDemo);
t.setDaemon(true); //设定守护线程
t.start(); //启动线程
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程运行结束");
}
}
线程的安全性问题
在多线程运行的环境下,程序运行的结果,和我们预期的结果不相符(排除错误问题),就可以看作是线程安全问题。
线程安全问题出现后,不太好解决,因为每次运行的结果可能都会不一样,问题不容易复现,解决比较困难,所以,我们应该在编写代码阶段,就将问题解决。
出现线程安全问题的根本原因,就是因为它们执行的步骤不是原子性的操作,所有会有其他线程读取到中间的执行步骤。
将来可以通过加上synchronized关键字解决线程安全问题
public class ThreadA {
//临界资源,多个线程共享访问的资源
int num;
public int getNum(){
return num;
}
public synchronized int updateNum(){
//临界资源发生写的操作
//先计算num+1
//把结果赋值给num
return num++;
}
}
public class ThreadB extends Thread {
ThreadA ta = new ThreadA();
@Override
public void run() {
while (true){
System.out.println("现在运行的是:"
+Thread.currentThread().getName() + ",num的值=====" +
ta.updateNum());
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ThreadB tb = new ThreadB();
new Thread(tb).start();
new Thread(tb).start();
new Thread(tb).start();
new Thread(tb).start();
}
}
线程安全问题发生的条件 ?
1,多线程的环境下
2,存在临界资源,并且多个线程会去共享访问这个资源
3,存在并发写临界资源的情况
线程同步
为什么要并发编程
充分利用多核CPU的计算能力,通过并发可以将多核CPU计算的性能发挥到极致
方便业务的拆分,提升系统的性能
并发编程可能会遇到各种问题,比如 :线程安全、死锁、内存泄露…
并发编程的三要素 :
**原子性:**一个或者多个操作,要么全部执行成功,要么全部执行失败
**可见性:**一个线程对共享变量的修改,另一个线程能够立刻看到
**有序性:**程序执行的顺序,按照代码先后顺序执行
并发编程的问题怎么解决?
使用线程同步,给线程执行的代码加锁
线程同步 :就是线程的一种等待机制,如果将来有多个线程需要访问同一个对象,就会进入这个对象的等待池,形成队列,前面的线程使用完了,下个线程再使用
Java中实现线程同步:
Java中,为了保证数据在方法中访问的正确性,加入了锁机制(synchronized、Lock),将来当一个线程获取到对象的锁之后,就把资源独占,其他线程必须等这个线程将锁释放,才能使用
线程同步的问题:
1,一个线程获得锁,其他的线程只能等待,挂起,相对来说,运行速度会慢一些
2,在多线程竞争下,加锁、释放锁、都会导致比较多的上下文切换、调度的延迟,引起性能问题
3,优先级高的线程,可能会等待一个优先级低的线程,导致优先级倒置,引起性能问题
Synchronized的用法
synchronized关键字,是Java中用来控制线程同步的
将来可以用来修饰方法、变量
synchronized在jdk1.6之后,被引入了大量的优化,用来减少锁操作的开销,所以现在synchronized比较常用
1,直接将synchronized用在方法上,称为同步方法
写法: public synchronized void method(){ }
synchronized修饰的方法,锁的是这个类的对象,每个对象对应了一把锁,每个synchronized方法,都需要获取到调用该方法的对象的锁,才能执行,否则线程就会阻塞
2,synchronized修饰静态方法 : 是给 Class类对象加锁,也就是给当前类加锁,也会作用于类的每个实例对象
写法: public synchronized static void method(){ }
3,synchronized用来修饰代码块,称为同步代码块
写法: synchronized(对象){}
修饰的对象可以称为同步监视器,一般使用都是将共享资源对象,放到这里,作为同步监视器
public class TicketRunnable implements Runnable {
private int ticketNum = 10;
boolean flag = true;
@Override
public void run() {
while (flag) {
buyTicket();
}
}
//写一个买票方法
public synchronized void buyTicket(){
//买票结束的判断
if (ticketNum <=0 ){
flag = false;
return;
}
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+"---->抢到了第"+ticketNum-- +"张票");
}
public static void main(String[] args) {
//在外面创建对象,将对象传入new Thread中,创建多个子线程
//这时候,多个子线程就会共享这个对象中的数据
TicketRunnable ticketRunnable = new TicketRunnable();
new Thread(ticketRunnable,"小明").start();
new Thread(ticketRunnable,"小红").start();
new Thread(ticketRunnable,"黄牛").start();
}
}
public class TicketRunnable implements Runnable {
private int ticketNum = 10;
boolean flag = true;
Object obj = new Object();
@Override
public void run() {
while (flag) {
buyTicket();
}
}
//写一个买票方法
public void buyTicket(){
synchronized (obj){
//买票结束的判断
if (ticketNum <=0 ){
flag = false;
return;
}
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+"---->抢到了第"+ticketNum-- +"张票");
}
}
public static void main(String[] args) {
//在外面创建对象,将对象传入new Thread中,创建多个子线程
//这时候,多个子线程就会共享这个对象中的数据
TicketRunnable ticketRunnable = new TicketRunnable();
new Thread(ticketRunnable,"小明").start();
new Thread(ticketRunnable,"小红").start();
new Thread(ticketRunnable,"黄牛").start();
}
}