【多线程】Java中的多线程

Java中的多线程

1.1Java中的多线程

Java程序的进程里有几个线程:主线程,垃圾回收线程(后台线程)等

在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。

Java支持多线程,当Java程序执行main方法的时候,就是在执行一个名字叫做main的线程,可以在main方法执行时,开启多个线程A,B,C,多个线程 main,A,B,C同时执行,相互抢夺CPU,Thread类是java.lang包下的一个常用类,每一个Thread类的对象,就代表一个处于某种状态的线程

1.2 如何理解多线程中的并行与并发

多个线程或进程”同时”运行只是感官上的一种表现。事实上进程和线程是并发运行的,操作系统的线程调度机制将时间划分为很多时间片段(时间片),尽可能均匀分配给正在运行的程序,获取CPU时间片的线程或进程得以被执行,其他则等待。而CPU则在这些进程或线程上来回切换运行。微观上所有进程和线程是走走停停的,宏观上都在运行,这种都运行的现象叫并发。

并行是指在多CPU或者多核心CPU的场景下,线程不共享CPU,每个线程一个CPU,同时执行多个任务的场景。

1.3 线程的生命周期以及状态变化

操作系统中的五种线程状态
【多线程】Java中的多线程_第1张图片

状态:

  1. 新建
  2. 就绪
  3. 运行
  4. 阻塞
  5. 死亡

Java中的六种线程状态

状态名称 说明
NEW 初始状态,线程刚被构建,但是还没有调用start()方法
RUNNABLE 运行状态,Java系统系统中将操作系统中的就绪和运行两种状态笼统地称为“运行中”
BLOCKED 阻塞状态,表示线程阻塞于锁
WAITTING 等待状态,表示线程进入等待状态,进入该状态表示当前线程做出一些特定动作(通知或者中断)
TIME_WAITTING 超时等待状态,该状态不同于等待状态,它可以在指定的时间后自行返回
TERMINATED 中止状态,表示当前线程已经执行完毕
public class TestThread {

    static String content;
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(){
            @Override
            public void run() {
                content="helloWrold";//正常进入run方法后就代表进入了运行状态,但是如果是直接调用t.run()就不是多线程了,只是一个普通方法
            };//run方法执行结束,线程处于死亡状态
        };
        t.start();//就绪状态,等待CPU分配时间片即可进入下一个运行状态
        //Thread.sleep(1000);//主线程阻塞
        t.join();//让t线程执行结束,主线程阻塞
        while (content==null){
            Thread.yield();//让当前线程放弃CPU,处于就绪的状态
        }
        System.out.println(content.toUpperCase());
    }
}

2. 如何理解线程安全与不安全

多个线程并发执行时,能否保证数据的正确性

如何保证线程安全,通过保证操作的原子性

3. 如何保证并发线程的安全性?

1.对共享进行限制访问(例如加锁:syncronized,Lock)

class counter01{
    private volatile int count;
    public synchronized void count(){//synchronized在jdk1.6之前性能差,所以才有Lock
        count++;
    }//排它锁,独占锁(只有一个线程能进来,其他线程拿不了),非公平锁(谁的线程优先级高,谁抢到就谁执行,有些线程会因为优先级低而一直无法执行)
}

class counter02{
    private volatile int count;
    private Lock lock=new ReentrantLock(true);//公平锁(拿不到锁的线程有机会拿到锁),公平性会导致性能差一些
    public void count(){
        lock.lock();//加锁
        try {
            count++;
        }finally {
            lock.unlock();//解锁
        }
    }
}

2.基于CAS(比较与交换)算法实现非阻塞同步(基于CPU硬件技术支持)

a)内存地址

b)期望数据值

c)需要更新的值

CAS算法支持无锁状态下的并发更新,但可能回出现ABA问题,长时间自旋问题

class counter03{
    //底层使用CAS算法实现,Atomic开头的类
    private AtomicInteger at=new AtomicInteger();
    public void count(){
        at.getAndIncrement();
    }
}

3.取消共享,每个线程一个对象实例(例如threadlocal)

Connection数据库连接对象

SimpleDateFormat日期格式化对象

SqlSession对象

都不允许共享,每个线程一个

class DateUtils{
    private static ThreadLocal<SimpleDateFormat> td=new ThreadLocal<>();
    public static  String format(Date date){
        SimpleDateFormat sdf = td.get();
        if(null!=sdf){
            return sdf.format(date);
        }else {
            sdf=new SimpleDateFormat("yyyy/MM/dd");
            td.set(sdf);
            return sdf.format(date);
        }

    }
}

说明:Java中的线程安全问题的主要关注点有3个:可见性,有序性,原子性

Java内存模型JMM解决了可见性和有序性问题,而锁解决了原子性问题。

4. Synchronized关键字应用及原理分析?

简介

Synchronized是排它锁的一种实现,支持可重入性

基于这种机制可以实现多线程在共享数据集上的同步(互斥与协作)

互斥:多线程在共享数据集上排队执行

协作:多线程在共享数据集上进行协作执行(通讯)

说明:

排他性:如果线程T1已经持有锁L,则不允许除T1外的任何线程持有该所L

重入性:如果线程T1已经持有锁L,则允许线程T1多次获取锁L,更确切的说,获取一次后,可多次进入锁

应用分析

  1. 修饰方法:同步方法(锁为当前实例或类对象,根据static判断)
  2. 修饰代码块:同步代码块(锁位代码块括号内配置的对象)

原理分析:基于Monitor监视器对象实现同步

java中所有对象都继承自Object,所有对象都有Monitor

Synchronized锁优化

为了减少获得锁和释放锁带来的性能消耗,JDK1.6以后的锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁装填、轻量级锁状态、重量级锁状态,这几个状态会随着竞争情况逐渐升级。

锁可以升级但不能降级,这种策略的目的是为了提高获得锁和释放锁的效率。

5. 如果理解volatile关键字的应用?

定义

一般用于修饰属性变量,volatile的底层是通过内存屏障来禁止指令重排序

  1. 保证共享变量的可见性(尤其是多核或多CPU的场景)
  2. 禁止指令重排序(例如count++底层会有三个步骤)
  3. 不能保证原子性

重排序有以下2个条件:

  1. 在单线程环境下不能改变程序运行的结果;
  2. 存在数据依赖关系的不允许重排序

应用场景分析

读写锁策略(一个写,并发读,类似读写锁)

class counter01{
    private volatile int count;
    public int getCount(){//read
        return count;
    }
        
    public synchronized void doCount(){//write
        count++;
    }
}

6. happen-before原则(禁止指令重排序)

在JMM中,如果一个操作的执行结果要对另一个操作可见,那么这两个操作之间必须存在happens-before关系,它是判断数据是否存在竞争,线程是否安全的主要依据

7. 如何理解悲观锁和乐观锁

JAVA中为了保证多线程并发访问的安全性,提供了基于锁的应用,可以分为2类:悲观锁和乐观锁

定义

  1. 悲观锁:假定会发生并发冲突,所以会屏蔽一切可违反数据完整性的操作,同一时刻只能有一个写操作。

    例如synchronized,Lock,ReadWriteLock,数据库悲观锁等实现

    数据库悲观锁:

    通过数据库锁机制实现,即对查询语句添加for update关键字。

    如下sql语句 select * from table where id = 1 for update 当一个请求A开启事务并执行此sql同时未提交事务时,另一个线程B发起请求,此时B将阻塞在加了锁的查询语句上,直到A请求的事务提交或者回滚,B才会继续执行,保证了访问的隔离性。

  2. 乐观锁:假定不会发生并发冲突,只在提交操作时检查是否违反数据完整性,多个线程可以并发执行写操作但是只有一个成功。

    基于CAS算法实现的Atomic包下的类,还有数据库乐观锁

    数据库乐观锁:

    2.1 借助数据库表增加一个版本号的字段version,每次更新一行记录,都使得该行版本号加一,开始更新之前先获取version的值,更新提交的时候带上之前获取的version值与当前version值作比较,如果不相等则说明version值发生了变化则检测到了并发冲突,本次操作执行失败,如果相等则操作执行成功。

    例如:update table set columnA = 1,version=version+1 where id=#{id} and version = #{oldVersion}

    2.2 借助行更新时间时间戳,检测方法则与方式1相似,即更新操作执行前先获取记录当前的更新时间,在提交更新时,检测当前更新时间是否与更新开始时获取的更新时间时间戳相等

应用场景

  1. 悲观锁适合写操作比较多的场景,加锁可以保证写操作时数据正确

  2. 乐观锁适合读操作比较多的场景,不加锁能使其读操作的性能大幅提升

8. 线程的上下文切换

​ 一个线程得到CPU执行时间是有限的,当此线程用完了为其分配的CPU时间以后,CPU会切换到下一个线程执行。

​ 在线程切换之前,线程需要将当前的状态进行保存,以便下次再次获得CPU时间片之后可以加载对应的状态以继续执行剩下的任务。而这个切换过程是需要韩飞时间的,会影响多线程程序的执行效率,所以在使用多线程时要减少线程的频繁切换。

减少线程的频繁切换的方式:

  1. 无锁编程
  2. CAS算法
  3. 使用最少线程,避免不必要的线程等待(如线程池的设计,核心线程数跟CPU核心数有关,最大线程数跟并发业务量有关,需要一个队列放阻塞的任务)
  4. 使用携程(Netty),一个专门负责其他线程任务调度和切换的线程,避免多线程

9. 如何理解死锁以及避免死锁

​ 多个线程互相等待被对方线程正在占用的锁,导致陷入彼此等待对方是否锁的状态,这个过程称之为死锁。

如何避免死锁

  1. 避免一个线程同时获取多个锁
  2. 避免一个线程在一个锁中获取其他的锁资源
  3. 使用定时锁来替换内部锁,如lock.tryLock(timeout)

你可能感兴趣的:(java,多线程)