java多线程开发学习

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

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

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


转载自:https://www.jianshu.com/p/8897d8c651de

3. 进程的概念

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

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

(4) 线程

(1) 线程的建立
Thread thread =new Thread();
thread.start();
//在这里启动后 会找到我们的run方法 去执行
(2) 终止线程
使用Thread.stop 该方法停止线程。但是该方法会直接终止线程,并且将立即释放这个线程所持有的锁,而这些锁是用来维持对象的唯一性的。如果数据写入到一半 锁时候后,另外一个线程 就会读取到该对象,但是这就读取到了一个不一致的对象这样就会造成程序出现问题。

从图中我们可知 对象有两个属性 分别为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);
    }
}

代码执行结果如图:
在这里插入图片描述
运行结果:
在这里插入图片描述
不用join方法的话,结果如下图:
在这里插入图片描述
运行结果:
在这里插入图片描述
从上面两个图可以看出来 如果不适用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);
        }
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值