欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Java并发(多线程)

程序员文章站 2022-03-25 15:04:37
Java并发(多线程)线程与进程进程:是程序的一次动态执行过程,是系统进行资源分配和调度的一个独立单位。进程例子—>电脑上的任务管理器进程的三种基本状态:就绪状态:进程分配到除CPU以外的所有必要资源执行状态:获得CPU,程序执行阻塞状态:请求I/O,申请缓存空间等线程:是一个比进程更小的执行单位。一个进程会有一个或多个线程。(1)线程的六种基本状态:New(新创建):new Thread(r) 创建一个新线程Runnable(可运行):一旦...

Java并发(多线程)

线程与进程

  1. 进程:是程序的一次动态执行过程,是系统进行资源分配和调度的一个独立单位。

    进程例子—>电脑上的任务管理器

    进程的三种基本状态:

    • 就绪状态:进程分配到除CPU以外的所有必要资源

    • 执行状态:获得CPU,程序执行

    • 阻塞状态:请求I/O,申请缓存空间等

Java并发(多线程)

  1. 线程:是一个比进程更小的执行单位。一个进程会有一个或多个线程。

    (1)线程的六种基本状态:

    • New(新创建):new Thread(r) 创建一个新线程

    • Runnable(可运行):一旦调用start方法,线程处于runnable状态。一个可运行的线程是否在运行,取决于操作系统给线程提供运行的时间。

    • Blocked(阻塞):线程因为某种原因放弃 CPU 使用权,暂时停止运行。在可执行状态下,如果调用sleep(),suspend(),wait() 等方法,线程都将进入阻塞状态

    • Waiting(等待):当处于运行状态下的线程调用 Thread 类的 wait() 方法时,线程进入等待状态。进入等待状态的线程必须调用 Thread 类的 notify() 方法才能被唤醒。notifyAll() 方法是将所有处于等待状态下的线程唤醒。

    • Timed waiting(计时等待):当线程等待另一个线程通知调度一个条件时,自己进入等待状态

    • Terminated(死亡):①run方法正常退出而自然死亡 ②没有捕获的异常终止了run方法而意外死亡

​ (2)线程的属性:①轻型实体②独立调度和分派的基本单位③可开发执行④共享资源进程

​ (3)线程的实现方式:①用户级线程:仅存在于用户空间中,线程管理全由用户程序完成,该线程系统的调度仍以进程为单位进行的;②内核支持线程:在内核空间实现

​ 3.多线程

​ 多线程:在程序中执行多个线程,每个线程完成一个功能,并与其他线程并发执行。使用多线程的主要目的是提高系统的资源利用率

​ 多线程面临的问题:内存泄露、死锁和上下文切换等

并发和并行

并发:同一时间段内同时执行多个任务。针对线程

并行:同一时刻内执行多个任务。针对进程

Java多线程的实现方式

两种:①继承Thread类 ②实现Runnable接口

继承Thread类

Thread 类的结构:

public class Thread implements Runnable
    //由此看出Thread类实现Runnable接口(为实现多继承)

Thread 类有如下两个常用构造方法:

  1. public Thread(String threadName)
  2. public Thread()

Thread 方法:

  1. public void start():开始执行线程;Java 虚拟机调用该线程的 run 方法

    1. public void run():如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回
    2. **public final void join(long millisec) ** :等待该线程终止的时间最长为 millis 毫秒
    3. public final void setPriority(int priority):更改线程的优先级。
    4. public final void setName(String name):改变线程名称,使之与参数 name 相同。
    5. public static void sleep(long millisec) :在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。

继承 Thread 类实现线程:

class MyThread extends Thread{ //MyThread类继承自 Thread 类
	public MyThread(String name){
		super(name);
	}
	public void run(){ //线程实现
		for(int i=1;i<=10;i++){
			System.out.println(getName()+"正在运行"+i);
		}
	}
}

public class ThreadTest {

	public static void main(String[] args) {
		MyThread mt1=new MyThread("线程1"); //创建线程
		MyThread mt2=new MyThread("线程2");
		mt1.start(); //start() 执行线程
		mt2.start();
	}

}

两个线程对象是交错运行的,哪个线程对象先抢到了 CPU 资源,哪个线程就可以运行,所以程序每次的运行结果是随机的。类中的 start() 方法表示此线程已经准备就绪,等待调用线程对象的 run() 方法。这个过程其实就是让系统安排一个时间来调用 Thread 中的 run() 方法,使线程得到运行,启动线程,具有异步执行的效果。

实现 Runnable 接口

如果要创建的线程类已经有一个父类,这时就不能再继承 Thread 类,因为 Java 不支持多继承,所以需要实现 Runnable 接口来应对这样的情况。

实现 Runnable 接口的语法格式如下:

public class thread extends Object implements Runnable

使用上述两种构造方法之一均可以将 Runnable 对象与 Thread 实例相关联。使用 Runnable 接口启动线程的基本步骤如下:

  1. 创建一个 Runnable 对象。

  2. 使用参数带 Runnable 对象的构造方法创建 Thread 实例。

  3. 调用 start() 方法启动线程。
    Java并发(多线程)

实现 Runnable 接口实现多线程:

class PrintRunnable implements Runnable { //实现Runnable接口
	int i = 1;
	@Override
	public void run() { // 重写run()方法,作为线程的操作主体 
		while (i <= 10)
			System.out.println(Thread.currentThread().getName() + "正在运行" + (i++));
        //Thread.currentThread().getName():获得当前线程名字
	}

}

public class Test {
	public static void main(String[] args) {
		PrintRunnable pr = new PrintRunnable(); //创建Runnable 对象
		Thread t1 = new Thread(pr); //使用参数带 Runnable 对象的构造方法创建 Thread 实例
		t1.start();//启动线程
		//PrintRunnable pr1 = new PrintRunnable();
		Thread t2 = new Thread(pr);
		t2.start();

	}

}

线程生命周期

线程的声明周期共有 6 种状态,分别是:新建 New、运行(可运行)Runnable、阻塞Blocked、计时等待Timed Waiting、等待Waiting 和终止Terminate

当你声明一个线程对象时,线程处于新建状态,系统不会为它分配资源,它只是一个空的线程对象

MyThread my1 = new MyThread();

调用 start() 方法时,线程就成为了可运行状态,至于是否是运行状态,则要看系统的调度了。

my1.start();

调用了 sleep() 方法、调用 wait() 方法和 IO 阻塞时,线程失去所占用资源之后,线程处于等待、计时等待或阻塞状态。

class MyThread implements Runnable{
	@Override
	public void run() {
		for(int i=1;i<=30;i++){
			System.out.println(Thread.currentThread().getName()+"执行第"+i+"次!");
			try {
				Thread.sleep(1000); //线程休眠1秒 
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}	
}
public class SleepDemo {
	public static void main(String[] args) {
		MyThread mt=new MyThread();
		Thread t=new Thread(mt);
		t.start();
		Thread t1=new Thread(mt);
		t1.start();
	}

}

run() 方法执行结束后,线程也就终止了。

Java 程序每次运行至少启动几个线程?

答:至少启动两个线程,每当使用 Java 命令执行一个类时,实际上都会启动一个 JVM,每个JVM实际上就是在操作系统中启动一个线程,Java 本身具备了垃圾的收集机制。所以在 Java 运行时至少会启动两个线程,一个是 main 线程,一个是垃圾回收线程。

线程的优先级

每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。

Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。

默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。

具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。

class MyThread extends Thread{
	private String name;
	public MyThread(String name){
		this.name=name;
	}
	public void run(){
		for(int i=1;i<=50;i++){
			System.out.println("线程"+name+"正在运行"+i);
		}
	}
}
public class PriorityDemo {

	public static void main(String[] args) {
		//获取主线程的优先级
		int mainPriority=Thread.currentThread().getPriority();
		//System.out.println("主线程的优先级为:"+mainPriority);
		MyThread mt1=new MyThread("线程1");// 实例化线程对象 
		MyThread mt2=new MyThread("线程2");
        MyThread mt3 = new Thread("线程3") ;  
		//mt1.setPriority(10);
		mt1.setPriority(Thread.MAX_PRIORITY);//优先级最高
		mt2.setPriority(Thread.MIN_PRIORITY);//优先级最低 
        mt3.setPriority(Thread.NORM_PRIORITY);//优先级中等
		mt2.start();
		mt1.start();
        mt3.start();
		//System.out.println("线程1的优先级为:"+mt1.getPriority());
	}

}

线程同步

当多个线程操作同一个对象时,就会出现线程安全问题,被多个线程同时操作的对象数据可能会发生错误。线程同步可以保证在同一个时刻该对象只被一个线程访问。

在Java中,synchronized关键字是用来控制线程同步的,在多线程的环境下,synchronized把代码段锁起来,不被多个线程同时执行。

关键字 synchronized 是一种互斥锁, 它确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,保证了线程对变量访问的可见性和原子性。它有三种使用方法:

  • 修饰普通方法,将会锁住当前实例对象。
  • 修饰静态方法,将会锁住当前类的 Class 对象。
  • 修饰代码块,将会锁住代码块中的对象。
  1. 修饰普通方法

    public class Test {
        // 修饰普通方法
        public synchronized void test() {
            // 需同步的代码  
        }
    }
    
  2. 修饰静态方法

    public class Test {
        // 修饰静态方法
        public static synchronized void test() {
            // 需同步的代码 
        }
    }
    
  3. 修饰代码块

    public class Test {
     public void test() {
        // 修饰代码块
        synchronized (this){
            // 需同步的代码
        }    
      }
    }
    

线程死锁

线程同步保证了资源共享操作的正确性,但是过多的同步会带来死锁问题。比如说一手交钱一手交货问题,你不交钱我不交货,互相等待,这就造成死锁。

​ 死锁:两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待的现象。

实例:(来自《Java多线程编程核心技术》)

package com.imooc.DealThread;

public class DealThread implements Runnable {

	public String username;
	public Object lock1 = new Object();
	public Object lock2 = new Object();

	public void setFlag(String username) {
		this.username = username;
	}

	@Override
	public void run() {
		if (username.equals("a")) {
			synchronized (lock1) {
				try {
					System.out.println("username = " + username);
					Thread.sleep(3000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				synchronized (lock2) {
					System.out.println("按lock1->lock2代码顺序执行了");
				}
			}
		}
		if (username.equals("b")) {
			synchronized (lock2) {
				try {
					System.out.println("username = " + username);
					Thread.sleep(3000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				synchronized (lock1) {
					System.out.println("按lock2->lock1代码顺序执行了");
				}
			}
		}
	}

}

测试:

package com.imooc.DealThread;

public class DeadThreadTest {

	public static void main(String[] args) {
		try {
			DealThread dtd1 = new DealThread();
			dtd1.setFlag("a");
			Thread thread1 = new Thread(dtd1);
			thread1.start();
			Thread.sleep(100);
			dtd1.setFlag("b");
			Thread thread2 = new Thread(dtd1);
			thread2.start();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

结果:

username = a
username = b

线程 a通过 synchronized (lock1) 获取了 lock1 的锁后休眠3s,执行线程b获取 lock2 的锁。两个线程休眠结束后,都想请求获取对方的资源,然后两个线程这样等待了下去,造成了死锁。

死锁产生的四个条件:

  • 互斥条件:在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求该资源,则请求者只能等待,直至占有该资源的进程用毕释放。
  • 请求和保持条件:指进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
  • 不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
  • 环路等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

如何避免线程死锁?

只需要破坏产生死锁的四个条件之一即可。

  • **破坏互斥条件:**这个条件我们没有办法破坏,因为我们用锁本身就是想让他们互斥的(临界资源需要互斥访问)。
  • **破坏请求与保持条件:**一次性申请所有的资源
  • **破坏不剥夺条件:**占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  • **破坏循环等待条件:**靠按顺序申请资源来预防。按照某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

问题

  1. 并发和并行的区别

    答:并发是同一时间段内同时执行多个任务。针对线程。多个处理器或多核处理器同时处理多个任务。

    ​ 并行是同一时刻内执行多个任务。针对进程。多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上看这些任务是同时进行的。

  2. 线程和进程的区别

    答:进程是程序的一次动态执行,线程是一个比进程更小的执行单位。一个进程会有一个或多个线程。

  3. 创建线程有哪几种方式?

    答:创建线程有三种方式:

    ①继承Thread重写run方法

    ②实现Runnable接口

    ③实现 Callable 接口

  4. 线程有哪些状态?

    答:线程的状态:

    · New:线程尚未启动

    · Runnable: 正在执行中

    · Blocked: 阻塞(被同步锁或者IO锁阻塞)

    · Waiting: 等待状态

    · Timed waiting: 等待指定的时间重新被唤醒的状态

    · Terminated:执行完成

  5. run() 和 start() 的区别
    答:start() 用于启动线程,run() 用于执行线程的运行时代码。run() 可以被重复调用,而 start() 只能被调用一次。多线程的工作是start() 做线程的相应准备工作,然后自动执行 run() 方法的内容。如果直接执行 run() 方法,被认为是在主线程下的 thread 的一个普通方法,这不是多线程工作。

  6. sleep() 和 wait()的区别
    答:①sleep() 方法没有释放锁,而 wait() 方法释放了锁。

    ②sleep() 来自 Thread,wait() 来自 Object。

    ③sleep() 时间到会自动恢复;wait() 可以使用 notify()/notifyAll()直接唤醒。

  7. 什么是死锁?
    答:当线程1持有锁a,并尝试去获取锁b ,此时锁b已被线程2 获取,同时线程2也想获取锁 a ,两个线程进入僵持状态,而发生的阻塞现象。

  8. 如何保证线程安全?
    答:①使用synchronized锁(自动锁)

    ​ ②使用Lock锁(手动锁)

    ​ ③使用安全类,例如java.util.concurrent 下的类

本文地址:https://blog.csdn.net/weixin_43795115/article/details/107694568