14.JavaSE_多线程(80%)

一、进程与线程的概念
1、进程:操作系统中一个程序的执行周期
2、线程:一个进程同时执行多个任务。通常来讲,每一个任务就称为一个线程。
3、进程与线程的比较:
  • 与进程相比,线程更加“轻量级”,创建、撤销一个线程比启动、撤销一个进程开销小得多。一个进程中的所有线程共享此进程的所有资源。
  • 没有进程就没有线程,进程一旦终止,其内的线程也将不复存在。
  • 进程是操作系统资源调度的基本单位,进程可以独享资源;线程需要依托进程提供的资源,无法独立申请操作系统资源,是OS任务执行的基本单位。

线程的状态:
在这里插入图片描述

二、java多线程实现
1、继承Thread类实现多线程
java.lang.Thread是线程操作的核心类。
新建一个线程最简单的方法就是直接继承Thread,而后覆写run()方法(相当于主线程的main()方法)。

无论哪种方式实现多线程。线程启动一定调用Thread类提供的start()方法!
线程start方法只能调用一次,多次调用会java.lang.IllegalThreadStateException异常。

class MyThread extends Thread{
    private String title;

    public MyThread(String title) {
        this.title = title;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(this.title + "、" + i);
        }
    }
}
public class Test0126 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread("线程1");
        MyThread myThread1 = new MyThread("线程2");
        MyThread myThread2 = new MyThread("线程3");
        myThread.start();
        myThread.start();
        myThread1.start();
        myThread2.start();
    }
}

运行结果:

线程1、0
Exception in thread “main” java.lang.IllegalThreadStateException
at java.lang.Thread.start(Thread.java:708)
线程1、1
at 多线程.Test0126.main(Test0126.java:24)
线程1、2
线程1、3
线程1、4
线程1、5
线程1、6
线程1、7
线程1、8
线程1、9

start(java方法) -> start0(JVM) -> 进行资源调度,系统分配(JVM) -> run(java方法)执行线程的具体操作任务

2、实现Runnable接口覆写run方法来实现多线程
class MyThread implements Runnable{
    private String title;

    public MyThread(String title) {
        this.title = title;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(this.title + "、" + i);
        }
    }
}
public class Test0126 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread("线程1");
        MyThread myThread1 = new MyThread("线程2");
        MyThread myThread2 = new MyThread("线程3");
        new Thread(myThread).start();
        new Thread(myThread1).start();
        new Thread(myThread2).start();
    }
}

运行结果:

线程1、0
线程1、1
线程1、2
线程1、3
线程1、4
线程1、5
线程2、0
线程2、1
线程2、2
线程2、3
线程2、4
线程2、5
线程2、6
线程2、7
线程2、8
线程2、9
线程3、0
线程1、6
线程3、1
线程3、2
线程3、3
线程3、4
线程3、5
线程3、6
线程3、7
线程3、8
线程3、9
线程1、7
线程1、8
线程1、9

使用匿名内部类进行Runnable对象创建

public class Test{
	public static void main(String[] args){
		new Thread(new Runnable(){
		@Override
		public void run(){
			System.out.println("Hello world");
		}
	}).start();
  }
}

使用Lambda表达式进行Runnable对象创建

public class Test{
	public static void main(String[] args){
		Runnable runnable = ()->System.out.println("Hello world");
		new Thread(runnable).start();
  }
}
3、继承Thread类与实现Runnable接口的关系

(1)Thread类与自定义线程类(实现了Runnable接口),是一个典型的代理设计模式。

  • Thread类负责辅助真实业务操作(资源调度,创建线程并启动)
  • 自定义线程类负责真实业务的实现(run方法具体要做啥事)

(2)使用Runnable接口实现的多线程程序类可以更好的描述共享的概念。

4、实现Callable < V > 覆写call< V >实现多线程(juc)—当线程有返回值时只能实现Callable实现多线程

juc-JDK1.5新增的并发程序编程包

java.util.concurrent.Callable< V >

实现callable接口而后覆写call()方法,有返回值
V call() throws Exception;

Future< V > get< V >
Future< V > 接口:
V get() throws InterruptException,ExecutionException;取得Callable接口的返回值

三、多线程常用操作方法
1、线程命名与取得
(1)通过构造方法在创建线程时设置线程名称
public Thread(String name);
public Thread(Runnable target,String name);

(2)取得线程名称
public final String getName();

(3)设置线程名称
public final synchronized void setName(String name);
2、线程休眠方法sleep()—单位为毫秒
public static native void sleep(long millis) throws InterruptException;

线程休眠:让当前线程暂缓执行,等到了预计时间后再恢复执行。
线程休眠会交出CPU,但是不会释放锁。

class MyThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(i);
        }
    }
}
public class Test0126{
    public static void main(String[] args) {
        Thread thread = new Thread(new MyThread());
        thread.start();
    }
}
3、线程让步(yield()方法)

暂停当前的线程对象,并执行其他线程。

yield()方法会让当前线程交出CPU,同样不会释放锁。但是yield()方法无法控制具体交出CPU的时间,并且yield()方法只能让拥有相同优先级的线程有获取CPU 的机会


class MyThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            Thread.yield();
            System.out.println(i);
        }
    }
}
public class Test0126{
    public static void main(String[] args) {
        Thread thread = new Thread(new MyThread());
        thread.start();
    }
}
4、等待其他进程终止:join()方法,会释放对象锁。

join方法只是对Object提供的wait()做了一层包装而已。
等待该线程终止,如果在主线程中调用该方法,会让主线程休眠,让调用该方法的线程先执行完毕后再恢复执行主线程。

5、线程停止

(1)设置标记位停止线程(**)

(2)调用Thread类的stop方法强制停止线程,该方法不安全已经被Deprecated

(3)调用Thread类的interrupt()方法中断线程

①interrupt()方法只是将线程状态置为中断状态而已,它不会中断一个正在运行的线程。此方法只是给线程传递一个中断信号,程序可以根据此信号来判断是否需要终止。
②当线程中使用wait、sleep、join导致此线程阻塞,则interrupt()会在线程中抛出InterruptException,并且将线程的中断状态由true置为false。

  • a.线程中没wait、sleep、join,调用进程interrupt只是将线程状态置为interrupt=true;
  • b.线程中有wait、sleep、join,调用interrupt会抛出InterruptException,在catch块中捕获该异常,然后退出。
6、线程优先级:[1,10]

线程优先级是指优先级越高,越有可能执行,但仅仅是有可能而已。

①设置优先级

public final void setPriority(int newPriority)

②取得优先级

public final int getPriority()

MAX_PRIORITY  = 10;
NORM_PRIORITY = 5;
MIN_PRIORITY = 1;

③主线程只是一个普通优先级而已。
④线程具有继承性:只是继承优先级而已。

从A线程启动B线程,则B和A的线程优先级是一样的。

7、守护线程
  • 守护线程是一种特殊的线程,又称为陪伴线程。Java中一共两种线程:用户线程与守护线程。
  • Thread类提供==isDaemon()==区别两种线程:返回false表示该线程为用户线程;否则为守护线程。典型守护线程就是垃圾回收线程。
  • 只要当前JVM进程中存在任何一个用户进程没有结束,守护线程就在工作;只有当最后一个用户线程结束时,守护线程才会随着JVM一同停止工作。
  • Thread提供setDaemon()经用户线程设置为守护线程。
三、同步问题

同步问题产生:使用内建锁synchronized进行同步处理。

1、synchronized实现同步处理(加锁操作)

(1)同步代码块:

①在方法中使用synchronized(对象this),一般可以锁定当前对象this
②synchronized(类名.class){}---------全局锁,类反射对象
③synchronized(任意对象obj){}

(2)同步方法:
在方法声明上加synchronized,表示此时只有一个线程能够进入同步方法。

  • 普通方法
  • 静态方法—全局锁
2、synchronized对象加解锁

synchronized(this)以及普通的synchronized方法,只能防止多个线程同时执行同一个对象的同步段。
synchronized锁的是括号中的对象而非代码。
全局锁:锁代码段。

  • 使用类的静态同步方法
    synchronized与static一起使用,此时锁的是当前使用的类而非对象。
  • 在代码块中锁当前Class对象
    synchronized(类名称.class){}
3、synchronized底层实现

(1)同步代码块底层实现:

执行同步代码块后首先要执行monitorenter指令,退出时执行monitorenter指令。使用synchronized实现同步,关键点就是要获取对象的监视器monitor对象。当线程获取到monitor对象后,才可以执行同步代码块,否则就只能等待。同一时刻只有一个线程可以获取到该对象的monitor监视器。

通常一个monitorenter指令会同时包含多个monitorexit指令。因为JVM要确保所获取的锁无论在正常执行路径或异常执行路径都能正确解锁。
(2)同步方法底层实现:

当使用synchrd标记方法时,字节码会出现访问标记ACC_SYNCHRONIZED。该标记表示在进入该方法时,JVM需要进行monitorenter操作。在退出该方法时,无论是否正常退出,JVM均需要进行minitorexit操作。

当JVM执行monitorenter时,如果目标对象monitor的计数器为0,表示此时该对象没有被其他线程所持有。此时JVM会将该锁对象的持有线程设置为当前线程,并且将monitor计数器+1。

在目标锁对象的计数器不为0的情况下,如果锁对象的持有线程是当前线程,JVM可以将计数器再次+1(可重入锁);否则需要等待,直到持有线程释放该锁。

当执行monitorexit时,JVM需将锁对象计数器-1。当计数器减为0时,代表该锁已被释放掉,唤醒所有正在等待的线程去竞争该锁。

四、同步问题产生:使用内建锁synchronized进行同步处理

对象锁(monitor):
对象锁机制是JDK1.6之前synchronized底层原理,又称为JDK1.6重量级锁,线程的阻塞以及唤醒均需要操作系统由用户态切换到内核态,开销非常之大,因此效率很低。
lock锁—JDK1.5(juc)—java语言层锁
JDK1.6之后对于内建锁的优化:锁等待时间变短而已,还是得排队

1、CAS(Compare and Swap)比较与交换机制

悲观锁:线程获取锁(JDK1.6之前内建锁)
是一种悲观锁的策略。假如每一次执行临界区代码(访问共享资源)都会产生冲突,所以当前线程获取到锁的同时也会阻塞其他未获取到锁的线程。
乐观锁(CAS操作,无锁操作):假如所有线程访问共享资源时不会出现冲突,由于不会出现冲突,自然就不会阻塞其他线程。因此线程就不会出现阻塞停顿的状态。出现冲突时,无所操作使用CAS(比较交换),来鉴别线程是否出现冲突,出现冲突就不断重试当前操作直到没有冲突为止。

①CAS操作过程:

CAS可以理解为CAS(V,0,N):
V:当前内存地址实际存放的值
0:预期值(旧值)
N:更新的新值

当V=0时,期望值与内存实际值相等,该值没有被任何其他线程修改过,即值0就是目前最新的值,因此可以将新值N赋值给V。
反之,如果V != 0,表明该值已经被其他线程修改过了,因此0值并不是当前最新值,返回V,无法修改。

当多个线程使用CAS操作时只有一个线程会成功,其余线程均失败。失败的线程会重新尝试(自旋)或挂起线程(阻塞)。

内建锁在老版本最大的问题在于:在存在线程竞争的情况下会出现线程的阻塞以及唤醒带来的性能问题,只是一种互斥同步(阻塞同步)。而CAS不是将线程挂起,当CAS失败后会进行一定的尝试操作并非耗时的将线程挂起,也叫做非阻塞同步。

②CAS的问题

  • ABA问题:CAS更新造成的问题
    解决:使用atomic AtomicStampedReference类解决,即添加版本号:1A->2B->3A
  • 自旋会浪费大量CPU资源。浪费时间
    与线程阻塞相比,自旋会浪费大量的处理器资源。因为当前线程仍处于运行状态,只不过跑的是无用指令。
    解决:自适应自旋:根据以往自旋等待时能否获取锁,来动态调整自旋的时间(循环次数)。如果在自旋时获取到锁,则会稍微增加下一次自旋的时长;否则就稍微减少下一次自旋时长。
  • 公平性:处于自旋比处于阻塞更容易获取到锁。
    内建锁无法实现公平机制,而lock体系可以实现公平锁。

①Java对象头:
Java对象头Mark Word字段存放内容:
对象的Hashcode
分代年龄
锁标记位
②JDK1.6之后一共四种状态锁:(锁只能升级不能降级)

  • 无锁 0 01
  • 偏向锁 1 01:一个线程一把锁
  • 轻量级锁 00:多个线程在不同时刻一把锁
  • 重量级锁 10:多个线程在同一时刻一把锁
  • GC标记:11
    根据竞争状态的激烈程度,锁会自动升级,锁不能降级(为了提高锁获取与释放的效率)
  • 偏向锁:只有一次CAS过程,出现在第一次加锁时。
    大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得。为了让线程获取锁的开销降低引入偏向锁。

偏向锁头部有Epoch字段值:表示此对象偏向锁的撤销次数。默认撤销40次以上,下次获取此对象时直接是轻量级锁,不再适用于偏向锁。20次

偏向锁时锁状态中最乐观的一种锁:从始至终只有一个线程请求一把锁。

偏向锁的获取:

当一个线程访问同步块并成功获取锁时,会在对象头和栈帧中的锁记录字段存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,直接进入。
当线程访问同步块失败时,使用CAS竞争锁,并将偏向锁升级为轻量级锁。

==偏向锁的撤销:==开销较大

偏向锁使用了一种等待竞争出现才释放锁的机制,所以当其他线程竞争偏向锁时,持有偏向锁的线程才会释放偏向锁,并将锁膨胀为轻量级锁(持有偏向锁的线程依然存活时)。
如果持有线程已经终止,则将锁对象的对象头设置为无锁状态。
JDK6之后偏向锁默认开启。

  • 轻量级锁:
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值