Java并发编程与多线程原理1

本文详细介绍了线程与并发的概念,分析了影响服务器吞吐量的因素,包括硬件和软件层面。讲解了并行与并发的区别,多线程的特点,以及Java中线程的创建、状态、终止和通信。此外,深入探讨了锁的原理,从简单的count++指令到锁的多种形态,如偏向锁、轻量级锁、重量级锁,以及锁的升级过程。最后,提到了线程间的wait/notify通信机制。

线程与并发

简单来说,并发是指单位时间内能够同时处理的请求数。
默认情况下Tomcat可以支持的最大请求数是 150,也就是同时支持150个并发。当超过这个并发数的时候,就会开始导致响应延迟,连接丢失等问 题。
**并发数定义:**每1秒所能处理的最大请求数量。

影响服务器吞吐量(并发量)的因素

影响服务器并发的因素,可以从硬件和软件2方面来说。

硬件

CPU、内存、磁盘、网络
硬件方面要做的就是,最大化的利用硬件资源

软件层面

线程数量、JVM内存分配大小、网络通信机制(BIO、NIO、AIO)、磁盘IO
那么线程数量如何影响到服务端并发量的?

并行和并发

并行 是指两个或者多个事件在同一时刻发生;多核心CPU同一时刻能够运行多个线程,例如一个4核CPU,同一时刻只能跑4个线程。

并发 是指两个或多个事件在同一时间间隔内发生。以线程为例,当前电脑的cpu是单核,也可以跑多线程。此时需要CPU通过不断分配时间片的方式来实现线程切换,由于切换的速度足够 快,我们很难感知到卡顿的过程。

多线程的特点

异步和并行
我们通过代码说明一下:

public class TestDemo extends Thread{
    @Override
    public void run() {
       //线程会执行的指令
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Come in");
    }
    public static void main(String[] args) {
        TestDemo testDemo=new TestDemo();
        testDemo.start();
        System.out.println("main thread");
    }
}

输出结果:

main thread
Come in

我们可以看到main线程是不会等到testDemo线程结束。同样main线程结束后,testDemo才输出结果。

Java中的线程

Runnable 接口
Thread 类
Callable/Future 带返回值的

public class CallableDemo implements Callable<String> {

    @Override
    public String call() throws Exception {
        System.out.println("come in");
        Thread.sleep(10000);
        return "SUCCESS";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService=Executors.newFixedThreadPool(1);
        CallableDemo callableDemo=new CallableDemo();
        Future<String> future=executorService.submit(callableDemo);
        System.out.println(future.get()); //阻塞
    }
}

future.get()方法是会阻塞的,他要等submit执行成功后才会返回结果。

Thread这个工具在哪些场景可以应用

网络请求分发的场景
文件导入
短信发送场景

线程的状态

需要注意的是,操作系统中的线程除去 new 和 terminated 状态,一个线程真实存在的状态,只有:
ready :表示线程已经被创建,正在等待系统调度分配CPU使用权。ready是一种临时的状态。
running :表示线程获得了CPU使用权,正在进行运算
waiting :表示线程等待(或者说挂起),让出CPU资源给其他线程使用
在加上新建状态和死亡状态,一共5种

在这里插入图片描述

public class Demo {

    public static void main(String[] args) {
        new Thread(()->{
            while(true){
                try {
                    TimeUnit.SECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"STATUS_01").start();  //阻塞状态

        new Thread(()->{
            while(true){
                synchronized (Demo.class){
                    try {
                        Demo.class.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },"STATUS_02").start(); //阻塞状态

        new Thread(new BlockedDemo(),"BLOCKED-DEMO-01").start();
        new Thread(new BlockedDemo(),"BLOCKED-DEMO-02").start();

    }
    static class BlockedDemo extends  Thread{
        @Override
        public void run() {
            synchronized (BlockedDemo.class){
                while(true){
                    try {
                        TimeUnit.SECONDS.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

jstack+进程号,查看当前进程中的线程状态:
在这里插入图片描述
我们可以看出有3种不同的阻塞:
WAITING
TIME_WAITING
BLOCKED
当然还有IO阻塞线程的情况。
waiting状态和blocked状态有什么区别?
blocked是锁阻塞,而waiting通过sleep、wait、join等来实现。二者没有太大的区别,只是操作过程不一样。

线程的启动

new Thread().start(); //启动一个线程
Thread t1=new Thread()
t1.run(); //调用实例方法
所以start()方法才是一个线程启动的入口,run方法只是调用实例的方法。
在Java和Jvm里面是没有线程的,是调用操作系统来创建的线程。

在这里插入图片描述
start0是一个native方法,native方法都是调用操作系统函数来实现的。(Java调用C的系统方法就是native)
为什么线程start()方法启动会先进入就绪状态?
因为start()方法启动后,会先经过操作系统的调度算法为线程分配相应的cpu资源。

线程的终止

线程什么情况下会终止?
1、run()方法执行结束
2、stop()方法来终止,但不建议使用。stop会强制终止线程。
3、interrupt

public class TestDemo extends Thread{
    @Override
    public void run() {
       //线程会执行的指令
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Come in");
    }

    public static void main(String[] args) {
        TestDemo testDemo=new TestDemo();
        testDemo.start();
        testDemo.stop(); //不建议 强制终止这个线程。
        //发送终止通知
    }
}

stop强制终止线程会导致数据不完整,所以不建议使用。
因而需要发送一个通知给线程,来说我要终止线程。

public class InterruptDemo implements Runnable{

    private int i=1;
    @Override
    public void run() {
//        Thread.currentThread().isInterrupted()=false;\
//        表示一个中断的标记  interrupted=fasle
        while(!Thread.currentThread().isInterrupted()){
            //
            System.out.println("Test:"+i++);
        }
        //
    }
    public static void main(String[] args) {
        Thread thread=new Thread(new InterruptDemo());
        thread.start();
        thread.interrupt(); //设置 interrupted=true;interrupted默认值是false;
    }
}

这样做就可以优雅的终止线程,但是你要在线程的run()方法中去引入Thread.currentThread().isInterrupted()来判断是否要终止线程。如果你没有 while(!Thread.currentThread().isInterrupted())的判断,那么是无法终止线程的。

interrupt()的作用

1、设置一个共享变量的值 true
2、唤醒处于阻塞状态下的线程。

public class InterruptDemo02 implements Runnable{
    @Override
    public void run() {
        while(!Thread.currentThread().isInterrupted()){ //false
            try {
                TimeUnit.SECONDS.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("processor End");
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(new InterruptDemo02());
        t1.start();
        t1.interrupt(); //有作用 true
    }
}

输出结果:

processor End

这个时候,如果我们在main线程加入一个睡眠阻塞,就会发现,我们的t1.interrupt()会失去作用,原因是t1.interrupt()会引发线程的复位。

public class InterruptDemo02 implements Runnable{
    @Override
    public void run() {
        while(!Thread.currentThread().isInterrupted()){ //false
            try {
                TimeUnit.SECONDS.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("processor End");
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(new InterruptDemo02());
        t1.start();
        Thread.sleep(1000);
        t1.interrupt(); //无作用 true。线程复位了,又把true改成了false。
                //Thread.interrupted() ;//复位
    }
}

t1线程正处在睡眠200s的阻塞过程之中,这个时候t1.interrupt()会引发了线程复位,先把false改为true,接着由于InterruptedException又把true改成了false。
所以,这种情况没有输出结果。t1线程会陷入死循环。
除了interrupt()会引发了线程复位之外,Thread.interrupted() 也可以引发复位。

interrupt本质是通过线程的一个共享变量_interrupted,来实现线程间相互通信。

 volatile jint _interrupted; // Thread.isInterrupted state

锁的原理

public class App {
    public static int count=0;
    public static void incr(){
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count++; 
        }
    public static void main( String[] args ) throws InterruptedException {
        for(int i=0;i<1000;i++){
            new Thread(()->App.incr()).start();
        }
        Thread.sleep(3000); //保证线程执行结束
        System.out.println("运行结果:"+count); }
}

运行结果输出:

运行结果:985

结果是小于等于1000的随机数。原因:可见性、原子性。

count++指令

count++在字节码层面实际上是3个指令。

 14: getstatic     #5                  // Field count:I
15: iconst_1
16: iadd
17: putstatic     #5

在这里插入图片描述
所以在线程切换的过程中,线程不安全导致数据不对。

锁(Synchronized)

可以修饰在方法层面和代码块层面

 class Test {
// 修饰非静态方法 synchronized void demo() {
// 临界区 }
// 修饰代码块
Object obj = new Object(); void demo01() {
synchronized(obj) { // 临界区
	} 
	}
}

锁的作用范围

synchronized有三种方式来加锁,不同的修饰类型,代表锁的控制粒度:

  1. 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
  2. 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
  3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
public class SynchronizedDemo {
    synchronized  void demo(){
    
    }
    Object lock=new Object();
    //只针对于当前对象实例有效.
    void demo2(){
         synchronized(lock){
         
         }
    }
    public static void main(String[] args) { 
        SynchronizedDemo synchronizedDemo=new SynchronizedDemo();
        //锁的互斥性。
        new Thread(()->{
            synchronizedDemo.demo();
        },"t1").start();
        new Thread(()->{
            synchronizedDemo.demo();
        },"t2").start();
         SynchronizedDemo synchronizedDemo2=new SynchronizedDemo();
        new Thread(()->{
            synchronizedDemo2.demo();
        },"t3").start();
    }
}

这是一种实例锁,只针对当前实例有效。例如,t1和t2互斥用一把锁,t3线程用的是另一把锁。
还有一种synchronized修饰在静态方法的,称之为类锁。对于这个类的所有对象都互斥。

锁的本质

锁的本质:共享资源

public class App {
    public static int count=0;
    static Object lock=new Object();
    public static void incr(){
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (lock) {
            count++;
        }
    }
    public static void main( String[] args ) throws InterruptedException {
        for(int i=0;i<1000;i++){
            new Thread(()->App.incr()).start();
        }
        Thread.sleep(3000); //保证线程执行结束
        System.out.println("运行结果:"+count);
    }
}

输出结果:

运行结果:1000

锁的存储

锁的存储和对象头有关系,对象头在Hotspot虚拟机中,表示了一个对象在内存中的布局。

在这里插入图片描述
以32位虚拟机为例
在这里插入图片描述
加锁会带来性能的开销
在这里插入图片描述
重量级锁会带来Blocked状态阻塞线程唤醒的开销。所以jdk1.6以后对锁进行了优化。

在这里插入图片描述
线程ThreadB会先尝试获取一个偏向锁,如果不成功再会去获取轻量级锁。如果获取轻量级锁也失败,则会尝试获得重量级锁,如果获得重量级锁失败则进入阻塞。这是锁的升级过程。
偏向锁和轻量级锁的目的是通过不加锁来保证线程安全。

锁的升级(锁的膨胀)

加锁一定会带来性能开销,怎么优化?
答案:不加锁(在不加锁的情况下解决线程安全问题)
所以先用偏向锁,不行再用轻量级锁,在阻塞(重量级锁)之前解决线程安全。

偏向锁

大多数情况下,锁不仅仅不存在多线程的竞争,而且总是由同一个线程多次获得。在这个背景下就设计了偏向锁。偏向锁,顾名思义,就是锁偏向于某个线程。

当一个线程访问加了同步锁的代码块时,会在对象头中存储当前线程的ID,后续这个线程进入和退出这 段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线 程的偏向锁。如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了。

引入偏向锁的目的是在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,进一步提高程序的运行性能。但在实际开发中,线程竞争还是比较多的,所以没有必要开启偏向锁。偏向锁默认是关闭的

轻量级锁(自旋锁)

如果偏向锁被关闭或者当前偏向锁已经已经被其他线程获取,那么这个时候如果有线程去抢占同步锁时,锁会升级到轻量级锁。
偏向锁进行了1次CAS,
而轻量级锁进行多次CAS。
原因:线程对锁的获取和释放时间很短,这种情况下多次CAS获取轻量级锁会比直接进入阻塞等待获取重量级锁要快一点。
当然不会无限制的多次CAS,JDK1.6开始默认是10次

CAS 乐观锁概念

比较预期的数据和原始数据是否一致,如果一致则修改,如果不一致则修改失效。

重量级锁

1、多个线程竞争同一个锁的时候,虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒 这些线程;
2、Java 线程的阻塞以及唤醒,都是依靠操作系统来完成的:os pthread_mutex_lock() ;
3、升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指 针,此时等待锁的线程都会进入阻塞状态。

 public static void main(String[] args) throws Exception {
    TestDemo testDemo = new TestDemo();
    Thread t1 = new Thread(() -> {
        synchronized (testDemo){
            System.out.println("t1 lock ing");
 System.out.println(ClassLayout.parseInstance(testDemo).toPrintable());
        }
    });
    t1.start();
    synchronized (testDemo){
        System.out.println("main lock ing");
        System.out.println(ClassLayout.parseInstance(testDemo).toPrintable());
    }
}

1、每一个JAVA对象都会与一个监视器monitor关联,我们可以把它理解成为一把锁,当一个线程想要执行 一段被synchronized修饰的同步方法或者代码块时,该线程得先获取到synchronized修饰的对象对应 的monitor。
2、monitorenter表示去获得一个对象监视器。
monitorexit表示释放monitor监视器的所有权,使得其他 被阻塞的线程可以尝试去获得这个监视器
3、monitor依赖操作系统的MutexLock(互斥锁)来实现的,线程被阻塞后便进入内核(Linux)调度状态,这 个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能
4、任意线程对Object(Object由synchronized保护)的访问,首先要获得Object的监视器。如果获取失 败,线程进入同步队列,线程状态变为BLOCKED。
当访问Object的前驱(获得了锁的线程)释放了 锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。
在这里插入图片描述
这个队列存储没有获得锁的线程,由1个链表来实现。
Thread1在success以后,则唤醒队列中的阻塞线程。
重量级锁是一个非公平锁,所谓非公平,就是可以插队。

锁的使用场景

偏向锁只有在第一次请求时采用CAS在锁对象的标记中记录当前线程的地址,在之后该线程再次进 入同步代码块时,不需要抢占锁,直接判断线程ID即可,这种适用于锁会被同一个线程多次抢占 的情况。
轻量级锁才用CAS操作,把锁对象的标记字段替换为一个指针指向当前线程栈帧中的 LockRecord,该工件存储锁对象原本的标记字段,它针对的是多个线程在不同时间段内申请通一 把锁的情况
重量级锁会阻塞、和唤醒加锁的线程,它适用于多个线程同时竞争同一把锁的情况。

线程的通信(wait/notify)

在Java中提供了wait/notify这个线程通信机制,用来实现线程的等待和唤醒。这个机制我们平时工作中用的少,但 是在很多底层源码中有用到。
比如以抢占锁为例,假设线程A持有锁,线程B再去抢占锁时,它需要等待 持有锁的线程释放之后才能抢占,那线程B怎么知道线程A什么时候释放呢?这个时候就可以采用通信机制(wait/notify)
在这里插入图片描述
wait()方法一定会释放锁。因为不释放锁,另外的线程是不会获取到锁。
sleep() 不会释放锁。sleep只是会释放CPU资源。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值