您的位置 首页 java

三石说:线程在java中的使用

在这篇我们将简单的看下,java如果创建 线程池 的,锁,以及线程同步问题和线程的一些面试题的

Java 中提供了两个种方式实现 多线程 ,分别是继承Thread类,和实现Runable接口

Runnable接口

 public class My thread  extends OtherClass implements Runnable{
  public void run() {
   System.out.println("MyThread.run()");
}
}
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();  

在Runnable接口实现后也是调用的Thread类进行实例化的,待 Thread 实例化后进行start方法,开启线程。我们直接进行看Thread类

Thread类

提供了setPriority(int newPriority)、getPriority()方法来设置和返回指定线程的优先级,其中setPriority()方法的参数可以是一个整数,范围是1~10之间,也可以使用Thread类的如下3个静态常量。

MAX_PRIORITY:其值是10。MIN_PRIORITY:其值是1 NORM_PRIORITY:其值是5。

下面程序使用了setPriority()方法来改变主线程的优先级,并使用该方法改变了两个线程的优先级,从而可以看到高优先级的线程将会获得更多的执行机会。

 public class PriorityTest extends Thread {
// 定义一个有参数的构造器,用于创建线程时指定name
    public PriorityTest(String name) {
        super(name);
    }
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println(getName() + ",其优先级是:"
                    + getPriority() + ",循环变量的值为:" + i);
        }
    }
    public static void main(String[] args) {
// 改变主线程的优先级Thread.currentThread().setPriority(6); for (int i=0 ; i < 30 ; i++ )
        {
            if (i == 10) {
                PriorityTest low = new PriorityTest("低级");
                low.start();
                System.out.println("创建之初的优先级:"
                        + low.getPriority());
// 设置该线程为最低优先级
                low.setPriority(Thread.MIN_PRIORITY);
            }
            if (i == 20) {
                PriorityTest high = new PriorityTest("高级");
                high.start();
                System.out.println("创建之初的优先级:"
                        + high.getPriority());
// 设置该线程为最高优先级
                high.setPriority(Thread.MAX_PRIORITY);
            }
        }
    }
}
  

上面程序中的第一行粗体字代码改变了主线程的优先级为6,这样由main线程所创建的子线程的优先级默认都是6,所以程序直接输出low、high两个线程的优先级时应该看到6。接着程序将low线程的优先级设为Priority.MIN_PRIORITY,将high线程的优先级设置为Priority.MAX_PRIORITY。下面展示Thread的过程。

ThreadLocal类:

ThreadLocal不是解决对象的共享访问问题,ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。

 public void set(T value) {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 实际存储的数据结构类型
        ThreadLocalMap map = getMap(t);
        // 如果存在map就直接set没有则直接创建并set
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
ThreadLocalMap getMap(Thread t) {
    // thread中维护了一个ThreadLocalMap
        return t.threadLocals;
    }
void createMap(Thread t, T firstValue) {
    //实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocals
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
从上面代码可以看出每个线程持有一个ThreadLocalMap对象。每一个新的线程Thread都会实例化一个ThreadLocalMap并赋值给成员变量threadLocals,使用时若已经存在threadLocals则直接使用已经存在的对象。
static class ThreadLocalMap {
        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
// ThreadLocalMap的构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            // 
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }
通过上面的代码不难看出在实例化ThreadLocalMap时创建了一个长度为16的Entry数组。通过hashCode与length位运算确定出一个索引值i,这个i就是被存储在table数组中的位置。        
  

对于某一 ThreadLocal 来讲,它的索引值i是确定的,在不同线程之间访问时访问的是不同的table数组的同一位置即都为table[i],只不过这个不同线程之间的table是独立的。

对于同一线程的不同ThreadLocal来讲,这些ThreadLocal实例共享一个table数组,然后每个ThreadLocal实例在table中的索引i是不同的。

当使用 ThreadLocal 维护变量时, ThreadLocal 为每个使用该变量的线程提供独立的变 量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

ThreadLocal和 synchronized 都是为了解决多线程中相同变量的访问冲突问题,不同的点是

  • Synchronized是通过线程等待,牺牲时间来解决访问冲突
  • ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值, 线程 外则不能访问到想要的值。

正因为ThreadLocal的线程隔离特性,使他的应用场景相对来说更为特殊一些。在android中Looper、ActivityThread以及AMS中都用到了ThreadLocal。当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。

线程池:

  1. 线程池的概念:

线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。

  1. 线程池的工作机制

2.1 在线程池的编程模式下,任务是提交给整个线程池,而不是直接提交给某个线程,线程池在拿到任务后,就在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲的线程。

2.1 一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。

  1. 使用线程池的原因:

多线程运行时间,系统不断的启动和关闭新线程,成本非常高,会过度消耗系统资源,以及过渡切换线程的危险,从而可能导致系统资源的崩溃。这时,线程池就是最好的选择了。

 package com.blueearth.bewemp.doc.config;
import java.util.concurrent.*;
/**
 * @user:
 * @date:2021/1/11
 * @Description:
 */
public class ExecutorsDemo {
    /**
     * 创建一个线程池,该线程池可重用固定数量的线程*在共享的无界队列上操作。在任何时候,最多* {@code nThreads}个线程将是活动的处理任务。
     * *如果在所有线程都处于活动状态时提交了其他任务,则*它们将在队列中等待,直到某个线程可用为止。
     * *如果任何线程由于执行过程中的失败而终止
     * *在关闭之前*如果需要执行一个新任务,将替换一个新线程。池中的线程将存在*,
     * 直到明确地{@link ExecutorService#shutdown shutdown}为止
     */
    /**
     *  阿里巴巴 规范
     * 强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
     * 说明:Executors返回的线程池对象的弊端如下:
     * 1)FixedThreadPool和SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
     * 2)CachedThreadPool和ScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
     */
    private static ExecutorService executorService = Executors.newFixedThreadPool(15);
    /**
     * Java中的B Lock ingQueue主要有两种实现,分别是ArrayBlockingQueue 和 LinkedBlockingQueue。
     *
     * ArrayBlockingQueue是一个用数组实现的有界阻塞队列,必须设置容量。
     *
     * LinkedBlockingQueue是一个用链表实现的有界阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。
     *
     * 这里的问题就出在:**不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。**也就是说,如果我们不设置LinkedBlockingQueue的容量的话,其默认容量将会是Integer.MAX_VALUE。
     *
     * 而newFixedThreadPool中创建LinkedBlockingQueue时,并未指定容量。此时,LinkedBlockingQueue就是一个无边界队列,对于一个无边界队列来说,是可以不断的向队列中加入任务的,这种情况下就有可能因为任务过多而导致内存溢出问题。
     *
     * 上面提到的问题主要体现在newFixedThreadPool和newSingleThreadExecutor两个工厂方法上,并不是说newCachedThreadPool和newScheduledThreadPool这两个方法就安全了,这两种方式创建的最大线程数可能是Integer.MAX_VALUE,而创建这么多线程,必然就有可能导致OOM
     *
     *  public static ExecutorService newFixedThreadPool(int nThreads) {
     *         return new ThreadPoolExecutor(nThreads, nThreads,
     *                                       0L, TimeUnit.MILLISECONDS,
     *                                       new LinkedBlockingQueue<Runnable>());
     *     }
     * @param args
     */
    // 推荐使用以下形式:
    // 这种情况下,一旦提交的线程数超过当前可用线程数时,就会抛出java.util.concurrent.RejectedExecutionException,
    // 这是因为当前线程池使用的队列是有边界队列,队列已经满了便无法继续处理新的请求。但是异常(Exception)总比发生错误(Error)要好
    private static  ExecutorService executor = new ThreadPoolExecutor(10,10,60L, TimeUnit.SECONDS,new ArrayBlockingQueue<>(10));
    public static void main(String[] args){
        for(int i=0;i<Integer.MAX_VALUE;i++){
            executorService.execute(new SubThread());
            System.out.println("主线程main:"+Thread.currentThread().getName()+":::"+i++);
        }
    }
}
class SubThread implements Runnable{
    @ Override 
    public void run() {
        try {
            // 睡100ms然后进行执行子线程
            Thread.sleep(100);
        }catch (InterruptedException e){
        }
        System.out.println("子线程:"+Thread.currentThread().getName());
    }
}
  

什么是 线程安全

  • 多线程环境下,
  • 对对象的访问不需加入额外的同步控制,
  • 操作的数据结果依然是正确的。

保证线程安全:

1、synchronized

synchronized关键字,就是用来控制线程同步的,保证我们的线程在多线程环境下,不被多个线程同时执行,确保我们数据的完整性,使用方法一般是加在方法上。

对于非静态代码块synchronized方法,锁的对象就是本身的this方法。

虽然加synchronized关键字,可以让我们的线程变得安全,但是我们在用的时候,也要注意缩小synchronized的使用范围,如果随意使用时很影响程序的性能,别的对象想拿到锁,结果你没用锁还一直把锁占用,这样就有点浪费资源。

2、Lock

先来说说它跟synchronized有什么区别吧,Lock是在Java1.6被引入进来的,Lock的引入让锁有了可操作性,什么意思?就是我们在需要的时候去手动地获取锁和释放锁,甚至我们还可以中断获取以及超时获取的同步特性,但是从使用上说Lock明显没有synchronized使用起来方便快捷。我们先来看下一般是如何使用的:

 private Lock lock = new ReentrantLock(); // ReentrantLock是Lock的子类
   private void method(Thread thread){
       lock.lock(); // 获取锁对象
       try {
           System.out.println("线程名:"+thread.getName() + "获得了锁");
           // Thread.sleep(2000);
       }catch(Exception e){
           e.printStackTrace();
       } finally {
           System.out.println("线程名:"+thread.getName() + "释放了锁");
           lock.unlock(); // 释放锁对象
       }
   }
  

这里跟synchronized不同的是,Lock获取的所对象需要我们亲自去进行释放,为了防止我们代码出现异常,所以我们的释放锁操作放在finally中,因为finally中的代码无论如何都是会执行的。

线程的面试题:

Q&A 1.什么是线程?

线程是程序执行运算的最小的基本单位

Q&A2.线程与进程的区别?

  • 进程是系统中正在运行的一个应用程序,表示资源分配的基本单位。又是调度运行的基本单位。
  • 一个线程只能属于一个进程,而一个进程可以拥有多个线程。
  • 同一进程的所有线程共享该进程的所有资源。同一进程的多个线程共享代码段。

Q&A3.如何保证线程安全?

加锁是最简单的直接的方式。synchronized关键字

Q&A4.如何使用线程?线程是如何启动的?

  • 实现runnable接口,
  • 实现Callable接口,
  • 继承Thread类,
  • 线程启动,重写run方法然后调用start()即可开启一个线程。

Q&A5.线程的几种状态?

5种状态:创建,就绪,运行状态,阻塞,死亡

线程创建时new状态,调用start()进入runnable就绪状态,争夺cpu资源后进入running状态,由于某种原因进入

(1)等待阻塞:运行的线程会释放占用的所有资源,jvm会把该线程放入“等待池”进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒,
(2)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入“锁池”中。
(3)其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。
当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
当线程正常执行结束会进入dead状态(一个未捕获的异常也会使线程终止)

  • yield()只是使当前线程重新回到runnable状态
  • sleep()会让出cpu,不会释放锁
  • join()会让出cpu,释放锁
  • wait() 和 notify() 方法与suspend()和 resume()的区别在于wait会释放锁,suspend不会释放锁
  • wait() 和 notify()只能运行在Synchronized代码块中,因为wait()需要释放锁,如果不在同步代码块中,就无锁可以释放
  • 当线程调用wait()方法后会进入等待队列(进入这个状态会释放所占有的所有资源,与阻塞状态不同),进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒

Q&A6.并发和并行的区别?

并发:同一时段,多个任务都在执行,

并行:单位时间内多个任务同时执行。

Q&A7.使用多线程带来什么问题可能?

并发是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、死锁

Q&A8.什么是死锁?如何避免死锁?

死锁:死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程

破坏死锁的四个条件:

  1. 互斥条件:该资源任意一个时刻只由一个线程占用。 (无法破坏)
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。(可以使用一次性申请所有资源)
  3. 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。(占用部分资源线程去申请其他资源,如果不能申请到就主动释放它占有的资源)
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。(按顺序申请资源。)

Q&A9.sleep()和wait()方法的区别?

  • sleep方法会让出cpu没有释放锁,wait方法释放了锁。
  • 两者都可以暂停线程的执行。
  • Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。

Q&A 介绍一下Syncronized锁,

synchronized修饰静态方法以及同步代码块的Synchronized用法锁的是类,线程想要执行对应的同步代码就需要或的类锁。

synchronized修饰成员方法,线程获取的是调用当前对象实例的对象锁。

Q&A 介绍一下Synchronized和lock,

synchronized是java的关键字,当用来修饰一个方法或者代码块的时候,能够保证在同一时刻最多只有一个线程执行该代码。jdk1.5后引入自旋锁,锁粗化,轻量级锁,偏向锁来优化关键字的性能。

Lock是一个接口,Synchronized发生异常时自动释放线程占有的锁,因此不会导致死锁的现象。Lock发生异常时需要通过unLock()去释放锁,则需要在使用finally块中释放锁,Lock可以让等待锁的线程响应中断,而synchronized却不行,synchronized时等待的线程会一直等待。Lock可以知道是否成功获取锁,而synchronized却无法办到。

Q&A 介绍一下volatile

volatile修饰的是保障有序性和可见性,比如我们写的代码不一定会按照我们书写的顺序来执行。

volatile是Java提供的轻量级的同步机制,比sync的开销要小

被volatile定义的变量,系统每次用到它时都是直接从主存中读取,而不是各个线程的工作内存

volatile可以像sync一样保持变量在多线程环境中是实时可见的

可见性:

每个线程都有自己的工作内存,每次线程执行时,会从主存获得变量的拷贝,对变量的操作是在线程的工作内存中进行,不同的线程之间不共享工作内存;对于volatile(sync,final)来说,打破了上述的规则,当线程修改了变量的值,其他线程可以立即知道该变量的改变。而对于普通变量,当一个线程修改了变量,需要将变量写回主存,其他线程从主存中读取变量后才对该线程可见

volatile具有sync的可见性,但是不具备原子性(解决java多线程的执行有序性)。volatile适用于多个变量之间或者某个变量当前值和修改之后值之间没有约束。因此,单独使用volatile还不足以实现计数器,互斥锁等。

在并发编程中谈及到的无非是可见性、有序性及原子性。而这里的Volatile只能够保证前两个性质,对于原子性还是不能保证的,只能通过锁的形式帮助他去解决原子性操作

这个就是java中的一些关于线程的知识了。在实际的工作中有可能我们用的不是很多,但是,我们在真正的开发中用到这个的还是有的,时刻准备着是不晚的。

文章来源:智云一二三科技

文章标题:三石说:线程在java中的使用

文章地址:https://www.zhihuclub.com/192484.shtml

关于作者: 智云科技

热门文章

网站地图