Java 多线程知识

参考链接:https://www.cnblogs.com/kingsleylam/p/6014441.html
https://blog.csdn.net/ly0724ok/article/details/117030234/
https://blog.csdn.net/jiayibingdong/article/details/124674922
https://blog.csdn.net/weixin_46574815/article/details/123636105
https://blog.csdn.net/JAYU_37/article/details/113842111
导致Java线程安全问题最主要的原因:
(1)多线程同时访问共享数据。
(2)多线程操作共享数据的过程中使用的计算方法不具备原子性。
解决线程安全问题的方案:
(1)避免共享数据。
(2)确保使用共享数据的原子性。
避免数据共享:
JAVA虚拟机在内存管理过程中将内存划分为不同区域,其中类成员变量存储在堆内存,方法变量存储在栈内存。堆内存在不同线程之间共享数据,有线程安全问题,而栈内存是线程独占的内存,不存在线程安全线问题。在允许的情况下,不使用成员变量、而是用方法变量、临时变量的话,可以避免共享数据,从而确保数据的线程安全问题。

public class A
{
	private int account = 0;
	public void cal()
	{
		for(int i=0;i<100;i++)
		{
			acount++;
		}
	}
	public int getAccount()
	{
		return account;
	}
}

多线程并发的情况下,account 有线程安全问题,而变量i是线程安全的、没有线程安全问题。
JMM内存模型: 描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。
在Java内存模型中:
(1)所有变量都存储在主内存中
(2)每个线程都有自己独立的工作内存,里面保存该线程使用到的变量副本。(主内存中该变量的一份拷贝)
(3)线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中进行读写。
(4)线程之间无法访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成。
共享变量实现可见性,必须经过如下两个步骤:
(1)线程中工作内存中的共享变量如果更新,需要将更新过后共享变量刷新到主内存中。
(2)主内存将最新的共享变量更新到其他工作内存中。
在这里插入图片描述
上图体现出线程只能与工作内存交互,不能直接访问主内存。当主内存中有一个共享变量X,则工作内存将X拷贝,线程操作工作内存中的X副本。

共享变量: 一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量。
原子性: 指一个操作不可被中断,要么全部执行成功,要么全部执行失败。同一时刻只能有一个线程对它进行操作。
可见性: 一个线程对共享变量值的修改能够及时被其他线程看到。

指令重排序:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中的各个语句的执行先后顺序同代码中的顺序一致,但它会保证程序最终执行结果和代码顺序执行的结果是一致的。
内存屏障: 用来禁止指令重排序。内存屏障分为两种:Load Barrier 和 Store Barrier,即读屏障和写屏障。作用: (1)阻止屏障两侧的指令重排序,即屏障下面的代码不能和屏障上面的代码交换顺序。(2)在有内存屏障的地方,线程修改完共享变量以后会马上把该变量从本地内存写回到主内存,并且让其他线程本地内存中该变量副本失效。
对于Load Barrier来说,在指令前插入
Load Barrier
,可以让高速缓存中的数据失效,强制从新从主内存加载数据;
对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,其他线程程可见

synchrnized

具有 原子性、可见性

对象锁(monitor)机制

进入同步代码块之前首先执行monitorenter指令,退出同步代码块执行monitorexit指令。当线程获得monitor后才能继续往下执行,否则就只能等待。
任何一个对象都有一个自己的monitor,线程执行对象的同步方法或同步块时,执行方法的线程必须获取该对象的monitor才能进入同步块和同步方法,若获取不到,则进入阻塞状态,进入同步队列。线程释放锁的时候会将值刷新到主内存中,其他线程获取锁时会强制从主内存中获取最新的值。——happen-before。类似线程通信。
**锁的重入性:**在同一锁程中,线程不需要再次获取同一把锁。synchrnized先天具有重入性。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加1,释放锁后就会将计数器减1。

CAS操作

悲观锁: 假设每一次执行临界区代码都会发生冲突,所以当前线程获取锁的过程会阻塞其他线程获取该锁。
乐观锁: 假设每一次执行临界区代码都不会发生冲突,所以不会阻塞其他线程操作。因此线程就不会出现阻塞停顿的状态。
CAS是用来鉴别乐观锁是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。

CAS操作过程

CAS包含三个值,分别为:V内存地址存放的实际值、O预期的值、N更新的新值。当V和O相同时,表明该值没有被其他线程更改过,可以把新值N赋值给V。相反,V和O不相同,表明该值已经被其他线程改过了,则该旧值不是最新版本的值了,所以不能将新值N赋值给V,返回V即可。当多个线程使用CAS操作一个变量时,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程。

CAS的问题

**ABA问题:**因为CAS会检查旧值有没有变化,这里存在这样一个有意思的问题。比如一个旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。解决方案可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。原来的变化路径A->B->A就变成了1A->2B->3C。
**自旋时间过长:**使用CAS时非阻塞同步,也就是说不会将线程挂起,会自旋(无非就是一个死循环)进行下一次尝试,如果这里自旋时间过长对性能是很大的消耗。
**只能保证一个共享变量的原子操作:**当对一个共享变量执行操作时CAS能保证其原子性,如果对多个共享变量进行操作,CAS就不能保证其原子性。有一个解决方案是利用对象整合多个共享变量,即一个类中的成员变量就是这几个共享变量。然后将这个对象做CAS操作就可以保证其原子性。atomic中提供了AtomicReference来保证引用对象之间的原子性。

voliate

轻量级同步机制。
voliate 是java中的一个关键字,用于修饰变量,被修饰的变量标记为线程共享,编译与运行时都会检查该变量,不会对其进行重排序。保证可见性有序性,不保证原子性
被voliate修饰的变量在编译成字节码时会多个lock前缀指令,该指令在执行过程中生成一个内存屏障,保证充排序后的指令不会越过内存屏障。即volatile之前的代码只会在volatile之前执行,volatile之后的代码只会在volatile之后执行。
满足以下一点,volatile 修饰的共享变量不加锁也能保证线程安全: 运算结果不依赖共享变量的当前值。(i++反例)。

Semaphore

继承关系
在这里插入图片描述
Semaphore用于管理信号量,在并发编程中可以控制访问同步代码的线程数量。

线程池

线程池是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后再创建线程后自动启动这些任务。
熟知的线程池: JDBC、数据库连接池(DataSource)、String(字符串常量池)

线程池的作用
  1. 降低资源消耗,通过重复利用已创建的线程降低创建和销毁的损耗。
  2. 提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行。
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池的基本原理

一个池子里面有很多线程,当再次需要执行某个任务时,不需要再创建线程了,而是直接从线程池中取出一个现成的线程供使用,即使该线程完成了任务,也不销毁线程,而是继续呆在线程池里准备迎接下一个任务。

为什么从池子中取要比创建线程快?

创建线程是在操作系统内核中完成的,涉及从用户态向内核态切换的操作,这个操作需要一定的开销。应用程序创建线程的是需要通过系统调用来完成的,进入操作系统内核中执行,也就是说,线程本质上就是PCB,是内核中的数据结构。创建线程是在内核中完成的,需要经历用户态->内核态的转变,而从线程池中取线程,把线程放回线程池,这一套是纯用户态的逻辑。

线程池主要参数
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
							  ThreadFactory threadFactory,
                              
                              RejectedExecutionHandler handler)
参数解释
corePoolSize线程池中的核心线程数。即使这些线程处于空闲状态也不会被销毁。任务提交到线程池后,首先会检查当前线程数是否达到了corePoolSIze,如果没有达到的话,会创建一个新线程来处理这个任务。
maximumPoolSize线程池允许的最大线程个数;当线程数达到corePoolSize后,如果有任务继续提交到线程池,会将任务缓存到工作队列中。如果队列也已满,则会创建一个新线程来进行处理。线程池不会无限制地去创建新线程,它会有一个最大线程数限制,这个数量由maximumPoolSize限制。
keepAliveTime空闲线程存活时间。一个线程如果处于空闲状态,并且当前线程数大于corePoolSize ,那么在keepAliveTime 后,这个空闲线程会被销毁。
unit空闲线程存活时间单位,keepAliveTime 的计量单位。
workQueue工作队列。新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk提供了四种工作队列:ArrayBlockingQueue、LinkedBlockingQuene、SynchronousQuene、PriorityBlockingQueue。
threadFactory创建一个新线程时使用的工厂,可以用来设定线程名,是否为daemon线程等等。
headler拒绝策略。当工作队列中的任务已经到达最大限制且线程池中的线程数也达到最大限制,这时有新任务再次提交会触发拒绝策略。jdk提供了4种拒绝策略:CallerRunsPolicy、AbortPolicy、DiscardPolicy、DiscardOldestPolicy。

**workQueue工作队列 **

  1. ArrayBlockingQueue 基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
  2. LinkedBlockingQuene 基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而基本不会去创建新线程直到maxPoolSize(很难达到Interger.MAX这个数),因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
  3. SynchronousQuene 一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
  4. PriorityBlockingQueue 具有优先级的无界阻塞队列,优先级通过参数Comparator实现。

handler 拒绝策略

  1. CallerRunsPolicy 该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。
  2. AbortPolicy 该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。
  3. DiscardPolicy 该策略下,直接丢弃任务,什么都不做。
  4. DiscardOldestPolicy 该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列。
线程池如何确定线程数量

在拥有N个处理器的系统上,如何确定线程数量。需要确定任务是CPU密集型应用还是IO密集型应用,还是混合型应用。如果是CPU密集型应用,则线程池大小设置为N+1。如果是IO密集型应用,则线程池大小设置为2N+1。
实际开发处理方案是需要实验验证的。针对自己的程序进行性能测试,对线程池设置不同的数目:0.5N、N、1.5N、2N… 然后分别记录每种情况下程序的一些核心性能指标和系统负载情况,最后选择一个合适的配置。

ConcurrentHashMap

ConcurrentHashMap 是HashMap的线程安全版本,其内部和HashMap一样,都是采用了数组+链表+红黑树的方式来实现。

JDK1.8版本ConcurrentHashMap做的改进

ConcurrentHashMap的key和value不允许为null。
在JDK1.7版本中,ConcurrentHashMap是由数组+Segment+分段锁实现,其内部分为一个个段(Segment)数组,Segment通过继承ReentrantLock来进行加锁,通过每次锁住一个segment来降低锁的粒度而保证每个segment内的操作的线程安全性,从而实现全局线程安全。
在JDK1.8中,对ConcurrentHashMap 做了优化,取消了分段锁的设计,而是通过cas操作和synchronized关键字来实现优化,扩容的时候利用一种分而治之的思想来提升扩容效率。

    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }
ReentrantLock

ReentrantLock 和 synchronized一样,都属于可重入锁。
ReentrantLock 的特点:

  • 可中断: synchronized 只能等待同步代码块执行结束,不可以中断,而reentrantlock可以调用线程中的interrupt方法来中断等待,继续执行下面的代码。
  • 可设置超时时间: 调用lock.trylock(),可以设置等待时间。
  • 可支持多个变量: 类似于调用wait()方法,不满足条件的线程进入waitset队列等待CPU随机调度,支持多个变量表示支持多个类似自定义waitset,这样就可以指定对象来唤醒了。
  • 可重入: 同一个线程如果获取到该锁资源,则它就有全力再次获得该锁。
  • 公平性: ReentrantLock可以开启公平锁模式。 ReentrantLock lock = new ReentrantLock(true);
    哲学家吃饭问题
public class Philosopher extends Thread{
    private Chopsticks left;
    private Chopsticks right;

    public Philosopher(String name, Chopsticks left, Chopsticks right) {
        super(name);
        this.left = left;
        this.right = right;
    }

    public void run()
    {
        while (true)
        {
            if(left.tryLock())
            {
                try {
                    if(right.tryLock())
                    {
                        try {
                            System.out.println(this.getName()+" eating ");
                            Thread.sleep(1000);
                            break;
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        } finally {
                            right.unlock();
                        }
                    }

                }finally {
                    left.unlock();
                }

            }
        }
    }

    public static void main(String[] args) {
        Chopsticks c1 = new Chopsticks();
        Chopsticks c2 = new Chopsticks();
        Chopsticks c3 = new Chopsticks();
        Chopsticks c4 = new Chopsticks();
        Chopsticks c5 = new Chopsticks();

        Philosopher  p1 = new Philosopher("t1",c1,c2);
        Philosopher  p2 = new Philosopher("t2",c2,c3);
        Philosopher  p3 = new Philosopher("t3",c3,c4);
        Philosopher  p4 = new Philosopher("t4",c4,c5);
        Philosopher  p5 = new Philosopher("t5",c5,c1);

        p1.start();
        p2.start();
        p3.start();
        p4.start();
        p5.start();

    }
}

条件变量

ReentrantLock支持多个条件变量。
使用方法:
1.await前需要获得锁。
2.await执行后,会释放锁,进入conditionObject等待。
3.await的线程被唤醒,重新竞争锁。
4.竞争锁成功后,从await后继续执行。

吸烟室和早餐室问题

public class ReentrantLockConditionTest {
    private static boolean hasCigarette = false;
    private static boolean hasBreakfast = false;

    private static ReentrantLock lock = new ReentrantLock();

    private static Condition waitCigaretteSet = lock.newCondition();
    private static Condition waitBreakfastSet = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {

        new Thread(()->{
            lock.lock();
            try {
                System.out.println("有烟嘛? "+hasCigarette);
                while (!hasCigarette)
                {
                    System.out.println("没有烟 需等待");
                    try {
                        waitCigaretteSet.await();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                System.out.println(Thread.currentThread().getName()+ " 可以开始工作了");
            } catch (RuntimeException e) {
                throw new RuntimeException(e);
            }finally {
                lock.unlock();
            }
        },"t1").start();


        new Thread(()->{
            lock.lock();
            try {
                System.out.println("有早餐嘛 "+hasBreakfast);
                while (!hasBreakfast)
                {
                    System.out.println("没有早餐 需等待");
                    try {
                        waitBreakfastSet.await();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }

                System.out.println(Thread.currentThread().getName()+ " 可以开始工作了");
            }catch (RuntimeException e)
            {
                throw new RuntimeException(e);
            }finally {
                lock.unlock();
            }
        },"t2").start();

        Thread.sleep(1000);

        new Thread(()->{
            try {
                lock.lock();
                hasCigarette=true;
                waitCigaretteSet.signal();

            }finally {
                lock.unlock();
            }
        }).start();

        Thread.sleep(1000);

        new Thread(()->{
            try {
                lock.lock();
                hasBreakfast=true;
                waitBreakfastSet.signal();
            }finally {
                lock.unlock();
            }

        }).start();

    }
}

可被中断

public class LockTest4 {
    static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {

        //t2首先获取锁 然后阻塞5s
        new Thread("t2"){
            @Override
            public void run() {
                try {
                    lock.lock();
                    System.out.println("t2获取了锁----");
                    System.out.println("睡眠5s 之后再执行");
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                    System.out.println("t2释放了锁----");
                }
            }
        }.start();

        TimeUnit.SECONDS.sleep(1); //主线程睡眠一秒

        //t1加锁失败因为被t2持有
        //t2首先获取锁 然后阻塞5s
        Thread t1 = new Thread(){
            @Override
            public void run() {
                try {
                    lock.lockInterruptibly();  //可以被打断的锁
                    System.out.println("t1获取了可被打断的锁--执行代码");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println("t1被打断了没有获取锁");
                    return;
                } finally {
                    try {
                        lock.unlock();
                        System.out.println("t1解锁");
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }
        };
        t1.start();


        //由于t1 可以被打断 故而1s之后打断t1 不在等待t2释放锁了
        try {
            System.out.println("主线程在1s后 打断t1");
            Thread.sleep(1000);
            t1.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java 多线程是指在一个程序中可以同时执行多个线程,并且每个线程独立运行,互不干扰。Java 多线程Java 平台提供的重要特,常用于提高程序并发能和解决复杂问题。 Java 多线程的重点知识点包括以下几个方面: 1. 线程的创建和启动:可以通过继承 Thread 类或实现 Runnable 接口来创建线程,并使用 start() 方法启动线程。 2. 线程的生命周期:线程的生命周期包括新建、就绪、运行、阻塞和死亡等阶段,可以通过调用 sleep()、wait()、join() 等方法来控制线程的状态转换。 3. 线程同步:多个线程之间可能会共享资源,为了保证数据的一致和避免冲突,需要使用同步机制,如 synchronized 关键字、Lock 接口等。 4. 线程间通信:多个线程之间可以通过共享内存或者消息传递的方式进行通信,如使用 wait()、notify()、notifyAll() 等方法。 5. 线程池:线程池可以管理和复用线程,减少线程的创建和销毁开销,提高系统的能。 6. 线程安全:在多线程环境下,存在资源竞争和线程安全问题,需要采取相应的措施来保证线程的安全,如使用 synchronized 关键字、volatile 关键字等。 7. 线程调度:Java 提供了线程调度器来控制线程的执行顺序和优先级,可以使用 yield()、join()、setPriority() 等方法进行调度。 8. 线程异常处理:在多线程环境下,线程的异常需要处理和捕获,可以使用 try-catch 块来捕获异常。 综上所述,Java 多线程Java 平台提供的重要特,掌握多线程的概念和相关知识点可以帮助开发者提高程序的并发能和解决复杂问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值