1. java的JMM 中的原子性、可见性、有序性

(1) 原子性 是指一个操作是不可中断的 。即使是多个线程一起执行的时候,一个线程一旦开始,就不会被其他线程干扰。 保证原子性 就是线程运行 不会被其他线程干扰 ,该线程中的内容也不被其他线程所影响 修改。
(2) 可见性 就是指 当一个线程修改了某一个共享变量的值,其他线程就能立即知道该变量被修改了。 在多线程中 全局变量可能 将变量值缓存在cache 中或者在寄存器中那么,某个修成修改了值 ,可能其他线程不能立即知道 该变量的修改。那么其他线程就会出现问题。
(3) 有序性 如果在串行上来说,有序性会按照顺序执行,但是在多线程中可能会出现执行 的时候 进行 指令重排的情况 。串行指令重排能导致语义一致,但是多线程的时候可能保证不了了

2. 指令不能进行重排 : Happen-Before 规则

java多线程顺序错乱_java多线程顺序错乱


3. 进程的概念

(1) 进程:官方的语言是计算机中的程序,关于某数据集合上的一次运行活动。使系统进行资源分配和调度的基本单位单位,是操作系统的基本单位,是操作系统结构的基础。在现在的线程设计的计算机结构中,进程是线程的容器,程序是指令数据及组织形式的描述,进程是程序的实体,但是总的来说 进程是线程的容器。在平常我们也会说线程是轻量级的进程,是程序执行的最小单位。使用多线程而不是用多进程进行并发设计的原因 线程间的切换与调度的成本远小于进程

(2) 线程的生命周期

线程 状态分为 NEW(新创建的),RUNNABLE(运行中),BLOCKED(同步块阻塞),WAITING(等待无限时间的等待),TIMED_WAITING(等待有限的时间 ),TERMINATED(结束) 六种。

java多线程顺序错乱_System_02

(4) 线程

(1) 线程的建立

Thread thread =new Thread();

thread.start();

//在这里启动后 会找到我们的run方法 去执行

(2) 终止线程

使用Thread.stop 该方法停止线程。但是该方法会直接终止线程,并且将立即释放这个线程所持有的锁,而这些锁是用来维持对象的唯一性的。如果数据写入到一半 锁时候后,另外一个线程 就会读取到该对象,但是这就读取到了一个不一致的对象这样就会造成程序出现问题。

java多线程顺序错乱_ide_03


从图中我们可知 对象有两个属性 分别为ID 和NAME , 条件为当ID和NAME 相等 时 对象是一致的,否则表示对象出错。 我们的写入线程总是把 ID和NAME 写成相同的值。 然后当我们在写线程把ID写入到对象中的时候 ,突然执行stop 操作,那么该对象的ID就会变成1 而NAME仍然为别的数值,这样就处于不一致的情况,写线程在这个时候把锁释放后,读线程争取到锁,开始读取数据,这样就读取到了错误数据。

(3) 线程中断

概念 :让目标线程停止执行,但是是高知目标线程希望线退出,具体退出由目标线程自己决定。
相关的方法,暂时只介绍Thread的方法

Thread.interrupt() //中断线程 也是告知目标线程中断,也就是设置中断标志位
Thread.isInterrupted() //判断是否被中断–通过上面方法设置的中断标志位来判断是否被中断
Thread.interrupted //判断是否被中断,并清楚中断状态,,, 用来判断当前线程的中断状态,同事清除中断标志位状态。

1) 程序实现

public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(){
            @Override
            public void run() {
                while(true){
                    //在这里判断 是否有中断位  ,又中断位了就将线程终止
                    if(Thread.currentThread().isInterrupted()){
                        System.out.println("中断了");
                        break;
                    }
                    Thread.yield();
                }
            }
        };
        t1.start();
        Thread.sleep(2000);
        t1.interrupt();  // 在这里进行通知中断
    }

(4) 通知(notity)与等待(wait)

  • 当一个线程执行wait方法的时候,那么当前线程就会停止继续执行,转为等待的状态,然后等其他线程执行notity 方法为止。执行如图所示,如果用notity是随机唤醒一个线程不是按照执行顺序唤醒线程。
  • wait 执行后会释放对应的监视器,其他线程会获得该监视器,然后执行notity 之后 会释放监视器,wait 重新获得监视器,继续执行程序。
public class SimpleWN {
    
    final static Object object =new Object();
    
    public static class T1 extends Thread{
        @Override
        public void run() {
            
            synchronized(object) {
                System.out.println(System.currentTimeMillis()+ "T1 start!");
                try {
                    System.out.println(System.currentTimeMillis() + "T1 wait for object");
                    object.wait();   //wait等待 然后等到notity来唤醒 ,但是notity唤醒也是随机的  并且wait 方法会释放目标对象的锁而下面的sleep方法就不会释放
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println(System.currentTimeMillis() + ": T1 end!");
            }
        }
    }
    
    public static class T2 extends Thread{
        @Override
        public void run() {
            synchronized (object) {
                System.out.println(System.currentTimeMillis()+"T2 start ! notity one thread");
                object.notify();
                System.out.println(System.currentTimeMillis()+"T2 end!");
                try {
                    Thread.sleep(2000);  //睡眠2秒
                } catch (Exception e) {
                    // TODO: handle exception
                }
            }
        }
    }
    public static void main(String[] args) {
        Thread thread1=new T1();
        Thread thread2=new T2();
        thread1.start();
        thread2.start();
    }
}

(5)挂起(suspend)和继续执行(resume)

  • 这是被废弃的方法,说明下原理:suspend 在导致线程暂停的通识不把锁的资源释放,其他线程 如果访问没有被释放的锁,那么也会受到牵连导致无法 继续执行。除非在该暂停的线程上进行了resume操作那么该线程才会继续执行,阻塞的其他线程也会继续执行。如果resume 操作在suspend前执行了那么被执行suspend的线程很难有机会再被继续执行。这种情况下很可能出现死锁的情况

(6)等待线程结束(join)和谦让(yield)

  • 我们在程序的执行中,很可能会出现多个线程之间有依赖关系的线程,只有当前一个线程执行完毕后才能继续执行以后的线程,那么我们可以使用Thread.join方法来实现 ,等待线程结束。看如下代码
  • yield方法:谦让,谦让,意思 当该方法执行后会让当前线程把cpu 让出来,到那时有一点需要注意的是让出cpu后该线程还会参与到cpu的争夺中,会不会分配到 这就不一定了
public class JoinMain {

    /**
     * volatile  变量的可见性 不可代替锁
     */
    private volatile static int i=0;
    
    public static class AddThread extends Thread{
        @Override
        public void run() {
            for(i=0;i<10000000;i++);
        }
    }
    
    public static void main(String[] args) throws Exception {
        AddThread thread= new AddThread();
        thread.start();
        thread.join(); //等待的意思,本质是让调用线程wait 在当前线程实例上调用的是wait(0) 这个方法
        System.out.println(i);
    }
}

代码执行结果如图:

java多线程顺序错乱_java多线程顺序错乱_04


运行结果:

java多线程顺序错乱_ide_05


不用join方法的话,结果如下图:

java多线程顺序错乱_java多线程顺序错乱_06


运行结果:

java多线程顺序错乱_java_07


从上面两个图可以看出来 如果不适用join 那么执行的结果会很小。执行join方法后会让该线程等待addThread线程执行完毕。

(7)volatile 与java内存模型(JMM)

  • java的内存模型都是围绕着原子性、有序性、还有可见性来展开的。
  • volatile 主要是用来告知虚拟机,被volatile 修饰的变量要注意,不要随意改动优化目标指令,使用该关键字是为了保证变量修改后会通知所有线程能看到该改动。保证变量的可见性。 volatile 并不能代表锁,无法保证复合操作的原子性。但是volatile 能保证元素的可见性和在一定程度上保证元素的有序性。保证修改的值会立即被更新到主存。当有其他线程需要读取时,它会去内存中读取新值。 volatitle 能保证一些的是禁止指令重排。
    (8)线程组
  • 在系统中我们可以把属于相同功能的线程放到一个线程组里面。
  • 关键字:ThreadGroup
public class ThreadGroupName implements Runnable {

    @Override
    public void run() {
        String groupName = Thread.currentThread().getThreadGroup().getName()
                +"-"+Thread.currentThread().getName();
        while(true) {
            System.out.println("I am "+groupName);
            try {
                Thread.sleep(3000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    public static void main(String[] args) {
        ThreadGroup  group= new ThreadGroup("PrintGroup");
        Thread  t1=new Thread(group,new ThreadGroupName(),"T1");
        Thread  t2 =new Thread(group,new ThreadGroupName(),"T2");
        t1.start();
        t2.start();
        
        System.out.println(group.activeCount());
        group.list();
    }
}

(9) 守护线程

  • 守护线程是在后台默默运行的一些系统性的服务 ,常见的守护线程有垃圾回收线程,JIT线程。 与之对应的线程就是 用户线程也可以被认为是系统的工作线程。 用户线程执行完毕后就会结束。在java中 只有守护线程的时候 java虚拟机就会自然的退出。
    关键字:thread.setDaemon(true); //设置为守护线程
public class DaemonDemo {
    public static class DaemonT extends Thread {
        @Override
        public void run() {
            while(true) {
                System.out.println("I am alive");
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        Thread thread= new DaemonT();
        thread.setDaemon(true);  //设置为守护线程
        thread.start();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

(10)线程优先级

  • 线程中有自己的优先级,优先级别高的线程 在争夺资源 的时候 处于优势的状态,也有可能出现抢占资源失败的情况
  • 如下面代码所示,线程中有优先级别设置,在java 中使用1到10 表示线程有限级别,我们一般用如图所示的变量来标识 线程优先级
  • 关键字:thread.setPriority(Thread.MAX_PRIORITY);
public class PriorityDemo {

    public static class HightPriority extends Thread{
        static int count =0;
        
        @Override
        public void run() {
            while(true) {
                 synchronized (PriorityDemo.class) {
                    count++;
                    if(count>1000000) {
                        System.out.println("HightPriority is complete");
                        break;
                    }
                }
            }
        }
    }
    
    public static class LowPriority extends Thread{
        static int count =0;
        @Override
        public void run() {
            while(true) {
                 synchronized (PriorityDemo.class) {
                    count++;
                    if(count>1000000) {
                        System.out.println("LowPriority is complete");
                        break;
                    }
                }
            }
        }
    }
    
    public static void main(String[] args) {
        Thread high =new HightPriority();
        LowPriority lowPriority=new  LowPriority();
        high.setPriority(Thread.MAX_PRIORITY);   //在这里设置 线程的优先级
        lowPriority.setPriority(Thread.MIN_PRIORITY);  
        lowPriority.start();
        high.start();
    }
}

5. 线程安全

(1)synchronized

  • 多线程在处理的过程中如果我们没有给程序进行过任何处理的话,在执行的过程中可能会出现多个线程对同一个数据同时进行修改,那么就可能会出现就修改一次的情况。比如程序中有i++,这个操作那么在执行过程中可能就会出现修改一次的情况。

    为了防止多线程操作出现问题,我们必须保证多个线程对线程不安全的数据访问完全同步。在Java 中提供了一个关键字synchronized来实现这个功能。
  • 关键字synchronized 的作用就是实现线程之间的同步,对需要同步的代码加锁,实现 只有一个线程进入同步代码块中。来保证线程间的安全性。大概有以下几种用法
    1)指定加锁对象,就是利用指定的对象在进入代码块之前获得 该对象。
    2)直接作用于实例方法上:利用当前的实例进行加锁,new 之后的对象
    3)直接作用于静态方法:对当前类加锁。 类变量。
    (2)并发下的线程安全
  • 如果程序中本来数据进行的都是正数操作,预期都是正数的话,那么如果出现负数,那么可能出现这个情况,可能就是内存溢出导致的问题。比如long 型 如果超过最大值那么就会变成负数。 这就是内存溢出的问题。线上有时候就会碰到该问题。需要注意!
    并发下的ArrayList
static ArrayList<Integer> al=new ArrayList<Integer>(10); 
   
    public static class AddThread implements Runnable{
        public void run() {
            for(int i= 0; i<1000000; i++) {
                al.add(i);
            }
        }
    }
    
    public static void main(String[] args) throws Exception {
        Thread t1 =new Thread(new AddThread());
        Thread t2 =new Thread(new AddThread());
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(al.size());
    }
}

这个代码执行完毕后,可能出现三种结果:

  • 正常结束,大小为200000
  • 出现异常的情况。下标越界情况
    这是因为ArrayList 在扩容的时候 ,内部的一致性被破坏掉。因为ArrayList 是线程不安全的,没有锁机制,导致在多个线程访问的时候 出现了不一致的内部状态,导致出现下标越界
  • 出现一个隐蔽的错误就是没有出现下标的越界的情况 ,只给了一个大小的值。出现这个情况的原因是多个线程在进行赋值的时候,对同一个位置进行赋值,在这种情况下就没有错误提示。我们可以使用Vector 代替ArrayList

并发下的hashMap

  • hashMap 同样也不是线程安全的 。会出现如下结果
    1)程序正常运行结果也符合预期。
    2)程序正常结束,但是不符合预期,小于我们需要的数据大小
    3)程序永远不会结束
  • 在验证过程中我们可以使用jps 命令来查看当前系统中java 的进程。如果想看具体 使用 可以使用 jps -help 有命令介绍。 这个是在linux上使用 一般会用jps -ml 显示内容和名称。
  • 在使用jps 后我们可以看到端口号 。然后使用jstack +端口号来看我们的运行的线程
  • java7线面的源码hashMap ,java8现在已经不是该循环
  • 解决方案可以使用ConCurrentHashMap代替

(3)错误的加锁问题

  • 在使用加锁的时候。我们不要使用锁对象是Integer ,String 这种对象去作为一个锁,可能造成加锁没有加成功的时候,因为如果我们在其他地方对其修改后,该对象的地址就会变成新的对象。那么锁就会失去作用

(4)同步控制

  • synchronized 扩展:重入锁
    1)重入锁来代替synchronized,在Jdk1.6以后 synchronized的性能与重入锁性能差不多
    2)重入锁的实现(ReenterLock.lock
public static ReentrantLock lookLock= new ReentrantLock();
    
    public static int i=0;
    
    public void run() {
        for(int j=0; j<100000;j++) {
            lookLock.lock();  //加锁
            lookLock.lock();
            try {
                i++;
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                lookLock.unlock();
                lookLock.unlock();
            }
        }
    }

    
    public static void main(String[] args) throws InterruptedException {
        ReenterLock  reenterLock=new ReenterLock();
        Thread t1=new Thread(reenterLock);
        Thread t2=new Thread(reenterLock);
        
        t1.start(); t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
  • 在上述代码中我们使用了重入锁来保护临界区资源i,确保程序的操作的安全性。我们在使用重入锁的时候需要显示的指定何时加锁,何时释放锁。必须释放锁不然其他线程没有机会访问临界区了。
  • 我们在代码上还实现了多次加锁的控制,同一个线程可以加入多个锁来控制 ,但是释放的时候加了 几个锁就要释放几个锁。不然其他线程也无法进入临界区。
    (5) 中断响应
  • 我们在使用synchronized 来加锁的话,那么结果只有两种可能 一是获得锁继续执行。二是 继续等待。但是我们使用重入锁就可以使其中断。
  • 锁申请等待限时
  • 除了我们在等待外部通知之外,避免死锁还有另外一种方法。就是限时等待。我们可以给定一个等待的时候后。如果线程很长时间拿不到锁,等待时间到了那么让其自动放弃.
public class TryLock implements Runnable {

    public static ReentrantLock  lock1 =new ReentrantLock();
    public static ReentrantLock  lock2 =new ReentrantLock();
    int lock;
    
    public TryLock(int lock) {
        this.lock=lock;
    }
    
    public void run() {
        
        if(lock==1) {
            while(true) {
                if(lock1.tryLock()) {
                     try {
                        try {
                            Thread.sleep(500);
                        } catch (Exception e) {
                            
                        }
                        if(lock2.tryLock()) {
                            try {
                                System.out.println(Thread.currentThread().getId() +":My Job done");
                                return ;
                            } finally {
                                lock2.unlock();
                            }
                        }
                    } finally {
                        lock1.unlock();
                    }
                }
            }
        }
        else {
            while(true) {
                if(lock2.tryLock()) {   
                try {
                    try {
                        Thread.sleep(500);
                    } catch (Exception e) {
                    }
                    
                    if(lock1.tryLock()) {
                        try {
                            System.out.println(Thread.currentThread().getId() +"My Job done");
                            return ;
                        } finally {
                            lock1.unlock();
                        }
                    }
                }   finally {
                    lock2.unlock();
                }
                }
            }
        }
    }   
    
    public static void main(String[] args) {
        TryLock r1=new TryLock(1);
        TryLock r2=new TryLock(2);
        Thread t1 =new Thread(r1);
        Thread t2 =new Thread(r2);
        t1.start();
        t2.start(); 
    }
}

(6) 公平锁

  • 大多数情况下,锁的申请都是非公平的。如果我们使用的是synchronized 来加锁,产生的锁就是非公平的。但是我们可以使用重入锁允许我们对其公平性进行设置。 方法ReentrantLock(boolean fair); 当 fair 为true 时,代表锁是公平的,但是使用公平锁 需要维护一个有序的队列,就会造成性能降低。因此我们在默认情况下不使用公平锁。
  • 整理ReentrantLock 的几个方法如下:
    1)lock():获得锁 ,吐过锁被占用 ,则等待;
    2)lockInterruptibly(): 获得锁,单会优先响应中断;
    3)tryLock(); 尝试获得锁,成功返回true,失败返回false;不等待立即返回。
    4)tryLock(long time,TimeUnit unit): 给定时间内获得锁。
    5)unlock(); 释放锁。

(7) 在重入锁实现中 主要包含三个元素:

  • 使用原子状态,原子操作使用CAS操作来存储当前锁的状态,判断锁是否被别的线程持有。
  • 等待队列。所有没有请求到锁的线程,会被放入等待队列中进行等待,待有线程释放后 系统就能从队列中唤醒一个线程,继续工作。
  • 阻塞park()和unpark,用来挂起和恢复线程。没有得到锁的线程将会被挂起。

(8) 重入锁的好搭档:Condition条件

  • Condition 条件与Object 的wait 和Object.notify 方法类似
  • Condition 有如下基本方法:
    1)await() 方法会使当前线程号等待,同时释放当前锁,当其他线程中使用signal() 或者使用signalAll()方法时,线程会重新获得锁并继续执行。或者当被中断的时候也能跳出等待。
    2)waitUninterruptibly() 与await类似 但是该方法不会再等待的过程中响应中断。
    3)singal() 方法用于唤醒 一个等待中的线程。singalAll唤醒所有在等待中的线程。
    演示代码如下:
public class ReenterLockCondition implements Runnable {

    public static ReentrantLock lock =new ReentrantLock();
    
    public static Condition condtionCondition =lock.newCondition(); //生成一个Condition对象
    
    public void run() {
            
        try {
            lock.lock();
            condtionCondition.await();  //执行这里的时候要求有相关的重入锁,在调用之后会释放锁
            System.out.println("Thread is going on");
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        ReenterLockCondition t2=new ReenterLockCondition();
        Thread t1=new Thread(t2);
        t1.start();
        
        Thread.sleep(2000);
        lock.lock();
        condtionCondition.signal();  //发出通告  ,并且要求其先获得相关锁。 
        lock.unlock();  //释放重入锁
    }
}
//相关具体更好的代码操作可以看ArrayBlockingQueuel例子中的put方法jdk1.7的

(9)允许多个线程同时访问:信号量

  • 信号量为多线程协作提供了更为强大的控制方法,广义的说是对锁的扩展。无论是内部锁synchronized还是重入锁ReentrantLock 。一次都只允许一个线程访问一个资源。而信号量可以指定多个线程同时访问某一个资源。
  • 构造方法如下:
    1)Semaphore(int permits);
    2)Semaphore(int permits,boolean fair) ; // 第二个参数可以指定时候公平
    3)在使用构造信号量的时候要指定准入的信号量的数量。同时可以申请多个许可
  • 关键字:Semaphore
    1)semp.acquire(); //获取信号量 每次能进来5个线程
    2)semp.release(); // 如果这里信号量泄露 没有释放 那么会导致进入临界区的线程数量越来越少。直到所有的线程不能再访问。
public class SemapDemo implements Runnable {

    final Semaphore semp =new Semaphore(5);
    
    public void run() {
        try {
            semp.acquire();  //获取信号量 每次能进来5个线程
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getId() + ":done!");
            semp.release();  // 如果这里信号量泄露 没有释放 那么会导致进入临界区的线程数量越来越少。直到所有的线程不能再访问
        } catch (Exception e) {
            e.printStackTrace();
        }       
    }

    public static void main(String[] args) {
                //开启20个线程 去访问,会发现是5个线程一组数据
        ExecutorService  executionn= Executors.newFixedThreadPool(20);
        final SemapDemo demo =new SemapDemo();
        for(int i=0;i<20;i++) {
            executionn.submit(demo);
        }
    }
}