zoukankan      html  css  js  c++  java
  • JAVA多线程下高并发的处理经验

    java中的线程:java中,每个线程都有一个调用栈存放在线程栈之中,一个java应用总是从main()函数开始运行,被称为主线程。一旦创建一个新的线程,就会产生一个线程栈。线程总体分为:用户线程和守护线程,当所有用户线程执行完毕的时候,JVM自动关闭。
    但是守候线程却不独立于JVM,守候线程一般是由操作系统或者用户自己创建的。 线程的生命周期:当一个线程被创建之后,进入新建状态,JVM则给他分配内存空间,并进行初始化操作。当线程对象调用了start()方法,该线程就处于就绪状态(可执行状态),JVM会为其创建方法调用栈、和程序计数器,处于可执行状态下的线程随时可以被cpu调度执行。
    CPU执行该线程的时候,该线程进入执行状态。执行过程中,该线程遇倒像wait()等待阻塞、以及synchronized锁同步阻塞或者调用线程的sleep()方法等进入一个阻塞状态,阻塞之后通过notify()或者notifyAll()方法唤醒重新获取对象锁之后再行进入就绪状态,
    等待cpu执行进去执行状态、当线程执行完或者return则线程正常结束,如果发生处理的运行时异常,则线程因为异常而结束。这是一个线程的整个运行的生命周期。如下图所示:
    
    线程的几种实现:
    
    1、继承Thread类,重写该类的run方法
    
    class MyThread extends Thread {
        
        private int i = 0;
     
        @Override
        public void run() {
            for (i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
            }
        }
    }
    public class ThreadTest {
     
        public static void main(String[] args) {
            Thread myThread1 = new MyThread(); // 创建一个新的线程  myThread1  此线程进入新建状态
            myThread1 .start();  // 调用start()方法使得线程进入可执行状态
        }
    }
    2、实现Runnable接口,并重写该接口的run()方法,该run()方法同样是线程执行体,创建Runnable实现类的实例,并以此实例作为Thread类的target来创建Thread对象,该Thread对象才是真正的线程对象
    
    class MyRunnable implements Runnable {
        private int i = 0;
     
        @Override
        public void run() {
            for (i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
            }
        }
    }
    public class ThreadTest {
     
        public static void main(String[] args) {
            Runnable myRunnable = new MyRunnable(); // 创建一个Runnable实现类的对象
     
            Thread thread1 = new Thread(myRunnable); // 将myRunnable作为Thread target创建新的线程
                   
            thread1.start(); // 调用start()方法使得线程进入就绪状态
                  
            }
        }
    }
    3、使用Callable和Future接口创建线程。具体是创建Callable接口的实现类,并实现clall()方法。并使用FutureTask类来包装Callable实现类的对象,且以此FutureTask对象作为Thread对象的target来创建线程
    
    public class ThreadTest {
     
        public static void main(String[] args) {
     
            Callable<Integer> myCallable = new MyCallable();    // 创建MyCallable对象
            FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask来包装MyCallable对象
     
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
                if (i == 30) {
                    Thread thread = new Thread(ft);   //FutureTask对象作为Thread对象的target创建新的线程
                    thread.start();                      //线程进入到就绪状态
                }
            }
     
            System.out.println("主线程for循环执行完毕..");
            
            try {
                int sum = ft.get();            //取得新创建的新线程中的call()方法返回的结果
                System.out.println("sum = " + sum);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
     
        }
    }
     
     
    class MyCallable implements Callable<Integer> {
        private int i = 0;
     
        // 与run()方法不同的是,call()方法具有返回值
        @Override
        public Integer call() {
            int sum = 0;
            for (; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
                sum += i;
            }
            return sum;
        }
     
    }
    继承Thread和实现Runnable接口实现多线程的区别:
    
    继承Thread类、实现Runnable接口,在程序开发中只要是多线程,肯定永远以实现Runnable接口为主,因为实现Runnable接口相比继承Thread类有如下优势:
    
    ​ 1、可以避免由于Java的单继承特性而带来的局限;
    
    ​ 2、增强程序的健壮性,代码能够被多个线程共享,代码与数据是独立的;
    
    ​ 3、适合多个相同程序代码的线程区处理同一资源的情况。
    
    线程的优先级别:
    
    java线程可以通过setPriority()方法对其设定一个优先级别,高优先级别的线程比低优先级别的线程有更高的几率得到先执行,优先级可以用0到10的整数表示,0为最低优先级别、10为最高优先级别。当线程调度器决定那个线程需要调度时,会根据这个优先级进行调度选择;1)Thread类有三个优先级静态常量:MAX_PRIORITY为10,为线程最高优先级;MIN_PRIORITY取值为1,为线程最低优先级;NORM_PRIORITY取值为5,为线程中间位置的优先级。默认情况下,线程的优先级为NORM_PRIORITY。2)一般来说优先级别高的线程先获取cpu资源先运行,但特殊情况下由于现在的计算器都是多核多线程的配置,有可能优先级低的线程先执行,具体的执行还是看JVM调度来决定。
    
    几种线程同步的方法:
    
    1、使用synchronized获取对象互斥锁:这种方式是最常用也比较安全的一种方式,采用synchronized修饰符实现的同步机制叫做互斥锁机制,它所获得的锁叫做互斥锁。每个对象都有一个monitor(锁标记),当线程拥有这个锁标记时才能访问这个资源,没有锁标记便进入锁池。任何一个对象系统都会为其创建一个互斥锁,这个锁是为了分配给线程的,防止打断原子操作。每个对象的锁只能分配给一个线程,因此叫做互斥锁。我们在使用同步的时候进来爸锁的粒度控制的精细一点,有时候没必要锁整个方法,只需要锁一个代码块即可达到我们的业务需求,这样避免其他线程阻塞时间过长造成性能上的影响。
    
    package per.thread;
     
    import java.io.IOException;
     
    public class Test {
        
        private int i = 0;
        private Object object = new Object();
         
        public static void main(String[] args) throws IOException  {
            
            Test test = new Test();
            
            Test.MyThread thread1 = test.new MyThread();
            Test.MyThread thread2 = test.new MyThread();
            thread1.start();
            thread2.start();
        } 
         
         
        class MyThread extends Thread{
            @Override
            public void run() {
                synchronized (object) {
                    i++;
                    System.out.println("i:"+i);
                    try {
                        System.out.println("线程"+Thread.currentThread().getName()+"进入睡眠状态");
                        Thread.currentThread().sleep(10000);
                    } catch (InterruptedException e) {
                        // TODO: handle exception
                    }
                    System.out.println("线程"+Thread.currentThread().getName()+"睡眠结束");
                    i++;
                    System.out.println("i:"+i);
                }
            }
        }
    }
    2、使用特殊域变量volatile实现线程同步:volatile修饰的变量是一种稍弱的同步机制,因为每个线程中的成员变量都会对这个对象的一个私有拷贝,每个线程获取的数据都是从私有拷贝内存中获取,而volatile修饰之后代表这个变量只能从共享内存中获取,禁止私有拷贝。在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制。从内存的可见性上来看,写入volatile变量相当于退出同步代码块,而读取volatile变量相当于进入同步代码块。但代码中过度依赖于volatile变量来控制同步状态,往往比使用锁更加不安全,使用同步机制会更安全一些。当且仅当满足以下所有条件时,才应该使用volatile变量: 1、对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。 2、该变量没有包含在具有其他变量的不变式中。
    
     class Bank {
                //需要同步的变量加上volatile
                private volatile int account = 100;
     
                public int getAccount() {
                    return account;
                }
                //这里不再需要synchronized 
                public void save(int money) {
                    account += money;
                }
            }
    3、使用重入锁Lock实现线程同步: 在jdk1.5以后java.util.concurrent.locks包下提供了这一种方式来实现同步访问。因为synchronized同步之后会存在一个阻塞的过程,如果这个阻塞的时间过久,严重影响我们代码的质量以及带来系统性能上的问题。因为我们需要一种机制,让等待的线程到达一定时间之后能够响应中断,这就是Lock的作用。另外lock还可以知道线程有没有成功获取到对象锁,synchronized无法做到。Lock比synchronized提供更多的功能。但要注意的是:1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
    
    package com.dylan.thread.ch2.c04.task;
     
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
     
    /**
     * This class simulates a print queue
     *
     */
    public class PrintQueue {
     
        /**
         *  创建一个ReentrantLock实例 
         */
        private final Lock queueLock=new ReentrantLock();
        
        /**
         * Method that prints a document
         * @param document document to print
         */
        public void printJob(Object document){
            //获得锁 
            queueLock.lock();
            
            try {
                Long duration=(long)(Math.random()*10000);
                System.out.printf("%s: PrintQueue: Printing a Job during %d seconds
    ",Thread.currentThread().getName(),(duration/1000));
                Thread.sleep(duration);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                //释放锁 
                queueLock.unlock();
            }
        }
    }
     注:关于Lock对象和synchronized关键字的选择: 
    
            a.最好两个都不用,使用一种java.util.concurrent包提供的机制,能够帮助用户处理所有与锁相关的代码。 
            b.如果synchronized关键字能满足用户的需求,就用synchronized,因为它能简化代码 
            c.如果需要更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁 
    
    4、使用ThreadLocal管理变量:如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响;ThreadLocal 类的常用方法:
    
    ThreadLocal() : 创建一个线程本地变量 
    get() : 返回此线程局部变量的当前线程副本中的值 
    initialValue() : 返回此线程局部变量的当前线程的"初始值" 
    set(T value) : 将此线程局部变量的当前线程副本中的值设置为value
      public class Bank{
                //使用ThreadLocal类管理共享变量account
                private static ThreadLocal<Integer> account = new ThreadLocal<Integer>(){
                    @Override
                    protected Integer initialValue(){
                        return 100;
                    }
                };
                public void save(int money){
                    account.set(account.get()+money);
                }
                public int getAccount(){
                    return account.get();
                }
            }
    5、使用阻塞队列实现线程同步:自从Java 1.5之后,在java.util.concurrent包下提供了若干个阻塞队列,或Redis消息队列等来实现同步等等。。。
    
    java多线程并发的业务场景:在互联网的大环境下很多场景都对并发要求越来越高,像天猫双十一秒杀、春运火车票抢票、微信抢红包、以及一些业务对某种资源的请求数量的控制、以及一些业务需要整个系统的输入输出顺序一致性等问题,这些都需要考虑到并发导致的数据安全性问题。
  • 相关阅读:
    【JDK】:java.lang.Integer源码解析
    使用truelicense实现用于JAVA工程license机制(包括license生成和验证)
    Android Button.getWidth()为0的问题
    nacos启动报错nacos Unable to start web server;
    Oracle字符集相关学习笔记记录
    Oracle触发器简单使用记录
    Oracle分析函数、窗口函数简单记录汇总
    Oracle远程数据建物化视图(materialized)创建简单记录,以及DBLINK的创建
    Oracle 行列转换函数pivot、unpivot的使用(二)
    Oracle递归查询(start with…connect by prior)
  • 原文地址:https://www.cnblogs.com/xianz666/p/13414841.html
Copyright © 2011-2022 走看看