• 1

  • 465

线程优化需要了解的一些点

Mellon

大家好

1星期前

前言

有感而发

目录

一、基本概念

1、CPU核心数以及线程数的关系

CPU核心数与线程数-知乎

Intel CPU有双核、四核、六核 等等,增加核心数有一点就是为了增加线程数,因为操作系统是通过线程来执行任务的,以前单核CPU下,与线程数的关系是 1:1,现在利用超线程技术,一般是 1:2,例如四核CPU,线程数应该有8个。Android手机架构不一样,有armX86,而且八核手机还分四大核、四小核,调度规则也不一致。

2、进程与线程

进程:程序运行资源分配的最小单位

线程CPU调度的最小单位,依赖于进程

假如**A(微信)**进程有A1A2线程 , **B(爱奇艺视频)**进程有B1B2B3线程,**C(腾讯视频)**进程有C1C2线程,这些线程都是可执行的。电脑上这三个软件窗口都打开了,CPU大概是如何调度执行这些?

在以前单核CPU单线程状态下,CPU会一个个执行,假设先执行A1线程、再B1线程、C2线程 (执行的线程顺序是不确定的),也就是电脑上你看到的已经打开的 ABC三个软件并不是时时刻刻在执行的,CPU会不断调度切换到其它软件去执行相关线程,切换速度很快,所以你能看到三个软件都在正常运行。如下图,一个状态下只执行一个线程,不会同时执行,这就是所谓的CPU轮转机制(RR调度)。

当开启的进程很多、线程很多情况下,例如两万个线程,CPU切换速度势必慢下来(上下文切换耗性能,每次切换都需要把上一个线程数据保存,把当前切换到的线程数据取出来),线程等待时间势必慢下来,这就导致电脑卡死现象原因之一,还和主频等因素也有关系,例如选打游戏电脑指标,你会关注主频。服务器需要稳定,会关注多核心。

3、并行与并发

并行:你和你女朋友同时各拿一个锅,一个开始煎鸡蛋、一个开始煎火腿,同时进行,这就叫做并行。或者是运动会百米冲刺,大家同时开跑。

并发:一般客户端不常会遇到,也可以说基本没有。一般说服务器的吞吐量指的就是并发。例如你的女朋友同时拿拿两个锅,一个煎鸡蛋,一个煎火腿,不断切换注意力到不同的锅上,在一定的时间内完成。或者是五条车道上,一定时间内的车流量,多少车经过。

并发数:是指在同一个时间点,同时请求服务的客户数量。

吞吐量(Throughput) :是指系统在单位时间内处理请求的数量。

二、常见开启线程的方式

1、Thread

继承Thread或者直接 new Thread()start后,执行完后线程就销毁了。

注意:new Thread 每次一开辟栈空间至少1M(默认栈大小1M + 栈溢出相应的检查16K),一个进程如果有100个线程,那可能就会消耗100M内存。

2、Runnable

自定义Runnable,传递给Thread

3、Callable

    private static  class WorkerThread  implements Callable<String>{

        @Override
        public String call() throws Exception {
            return "Demo Test";
        }
    }

    //开启
    WorkerThread  workerThread = new WorkerThread();
    FutureTask<String>  task =new FutureTask<>(workerThread);
    new Thread(task).start();
    task.get();//取返回值 阻塞
复制代码

三、线程关闭方式

Thread提供了stopstopSelf方法,不过都标记了 @Deprecated 过时,不建议使用,例如图片下载,停止线程,图片可能并没有下载成功,stop就会造成一些不避免的错误,还有一些内存释放操作。

现实中,大多数是通过在Threadrun方法里去判断是否需要停止线程,目的就是促使run方法执行完毕。如下

   public   class MyThread extends Thread {

        @Override
        public void run() {
            //业务逻辑

            if(interrupted()){ 
               //直接return
               return;
            }
        }
    }

    MyThread thread = new MyThread();
    thread.start();
    //业务
    thread.interrupt();//发送中断消息
复制代码

如果是自定义Runnable,则在run方法中,通过 Thread.currentThread().isInterrupted() 来判断。

这里需要注意一点

try{
    Thread.sleep(100);
}catch (InterruptedException  e){

}
复制代码

如果在Thread.sleep期间,执行了 interrupt 方法,将会触发InterruptedException 异常,会清除中断信号

四、线程的生命周期

图中setDemon是指让线程设置为守护线程,主线程挂了,守护线程也就挂了。

yield 让出执行权,进入到就绪状态(可执行状态)

1、sleep和wait的区别

sleepThread的方法,waitObject的方法,底层都是native方法。

sleep是休眠,等休眠时间一过,才有执行权资格,但是只有又有了资格,不代表马上就能被执行,什么时候执行取决于操作系统的调用。

wait是等待,等待别人来唤醒,唤醒后,才有执行权的资格,和sleep一样,什么时候执行取决于操作系统的调度。

sleep可以无条件马上休眠,对象锁依然保持,而wait的使用,是因为某些条件下才使用的,并且wait会导致当前线程放弃对象的锁,进入对象的等待池。

2、线程是否能被强制中断?

可以,但是不推荐,之前提过,一般通过 interupt 方法来处理,该方法属于协作式停止线程,并不是抢占式。

3、如何控制线程执行顺序?

利用join方法,例如

public static main(String[] args){
   ....
   thread1.start();
   thread.join();
   thread2.start();
}
复制代码

程序在main线程中调用thread1join方法,main线程放弃CPU控制权,返回thread1线程并执行thread1线程完毕,然后CPU执行权再放开。

4、既然可以控制线程执行顺序,那么在Java中能不能指定CPU去执行某个线程?

不能,Java是不能处理的,唯一能够去干预的就是C语言调用内核API去指定。

5、项目开发中,会考虑线程的优先级吗?

一般不会考虑优先级,因为线程的优先级依赖于系统平台,所以这个优先级无法对号入座,所设定的优先级可能是不稳定的,有风险。例如Java线程优先级有十级,android1000级,操作系统如何只有2-3级,如果是跨平台开发,基本不考虑,如果只是在某个特定端上开发,可以考虑。

五、锁

不考虑Java并发的CAS乐观锁,悲观锁细节

锁可以分为对象锁、类锁、显示锁,对应 synchronizedReentrantLock

1、Synchronized

public class Test {
    private long count = 0;
    private Object object = new Object();
    private String str = new String();
    //对象锁
    public void fun() {
        synchronized (object) {
            count++;
        }

    }
  //对象锁
    public synchronized void fun2() {
        count++;
    }

    public void fun3() {
        synchronized (this) {
            count++;
        }
    }
    //类锁
    public static  synchronized void fun4(){

    }
}
复制代码

synchronized 可以用在方法、变量、方法上面。

synchronized 标记的代码,编译器自动会生成 monitorentermonitorexit,执行enter指令尝试获取该对象的锁时会检查锁状态标志,偏向线程ID等类型西,同时利用计数器加1,执行了monitorexit后,计数器减1,计数器为0时自动释放锁。获取对象锁失败,那么当前线程就要阻塞等待,直到对象锁被另外一个线程释放。

为什么锁对象可以?

对象头包含锁,每个对象都持有自己的Monitor锁。

java对象在布局中分为3部分,对象头、实例数据、对其填充。在new创建一个对象时,jvm会在堆中创建一个instanceOopDesc对象,其中由 _mark_metadata 一起组成了对象头_metadata主要保存类元数据, _mark属性是markOop类型数据,我们都称为mark word标识,存储了hashCode分代年龄,锁标识,是否是偏向锁等等,然后在32位的JVM中,重量级锁标志位占10位,剩下30位是指向互斥锁的指针Monitor,ObjectMonitor是他的实现,每个对象都有。

说到synchronized,就有必要提一提单例

public class Singleton {
    private static Singleton singleton;

    private Singleton() {

    }

    public static synchronized Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }

}
复制代码

上面这种写法是线程安全的,唯一不足是每次只能有一个线程访问,会导致一定的性能损失

public class Singleton {
    private volatile static Singleton singleton;

    private Singleton() {

    }

    public static  Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class){
                if(singleton == null){
                    singleton = new Singleton(); // 1
                }
            }
        }
        return singleton;
    }

}
复制代码

上面这种事DCL线程安全写法。假如没有使用volatile关键字,会造成什么问题呢?

首先我们直到创建一个对象的过程

  • 分配内存空间

  • 初始化对象

  • 将对象空间的地址赋值给引用

如果没有使用volatile关键字,在线程A执行到 1 处的方法时,由于指令重排的原因,会出现执行了创建对象的第一步、第三步,此时singleton确实不会为空,并且这时候另一个线程执行到getInstance方法,就直接返回了,而此时对象的构造方法并没有被调用,会出现第二个线程拿到的数据是错乱的。

加了volatile关键字,避免了JVM对其的指令重排,完全按创建对象的顺序来,就不会出现此类问题,当然你也可以通过内部类的形式来创建单例

public class Singleton {
    private volatile static Singleton singleton;

    private Singleton() {

    }

    public static  Singleton getInstance() {
         return InnerHolder.S_INSTANCE;
    }
    private static class InnerHolder{
        private static final Singleton S_INSTANCE = new Singleton();
    }

}
复制代码

首次加载Singleton类时并不会初始化Instance,只有在第一次调用getInstance()时会导致虚拟机加载SingleHolder类。

虚拟机会保证一个类的构造器<clint>方法在多线程环境中被正确加载,同步,如果多个线程同时去初始化一个类,那么只有一个类去执行,其他线程都需要阻塞等待,直到活动线程<clint>方法完毕,所以通过内部类创建单例是线程安全的。

2、ReentrantLock

​try{
    lock.lock();
    //业务逻辑
    ....
}finally{
    lock.unlock();
}
复制代码

ReentrantLock和读写锁ReentrantReadWriteLock,它们都实现Lock接口

ReentrantLock的构造函数

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
复制代码

fairtrue代表公平锁,false代表非公平锁,默认是false

那么非公平和公平有什么区别呢?

如果使用公平锁,如果多个线程访问同一个方法,先申请访问的会先执行,其它的线程挂起,例如第一申请的执行完该方法后,就会上下文切换,轮到第二申请的线程执行该方法了。如果使用的是非公平锁,不会管申请的先后顺序,第一申请的执行完该方法后,将由CPU调度,决定哪个线程执行。从源码角度来说,公平锁,是会判断队列中是否还有其他线程在等待尝试获取锁,会按队列的先后顺序来获取锁(线程上下文切换),非公平锁,是直接尝试获取锁的。

所以公平锁相对非公平锁会耗时间、耗性能,非公平锁的效率更高。

3、可重入锁

public class LockTest {
    private static  int count= 0;
    public static synchronized void incrs(){
        System.out.println("count -> "+count);
        count ++;
        if(count < 3) {
            incrs();
            //注释 1  
            int copy = count;
            System.out.println("copy -> "+copy);
        }
    }
    public static void main(String[] args) {
     incrs();
    }
}
复制代码

这段代码结果是

count -> 0
count -> 1
count -> 2
copy -> 3
copy -> 3
复制代码

可以看出 synchronized 是可重入锁,并且 注释 1 处代码只在最后执行一遍。如果不是可重入锁,那么将会导致死锁。ReentrantLock 也是可重入锁。

4、synchronized 的缺点

不支持中断和超时,也就是说通过synchronized一旦被阻塞住,如果一直无法获取到所资源就会一直被阻塞,即使中断也没用,这对并发系统的性能影响太大了

Lock支持中断和超时、还支持尝试机制获取锁,对synchronized进行了很好的扩展

5、notify 和 notifyAll 选哪个

public class LockTest {
    static class Product {
        private String name;
        private boolean isCreated;

        public synchronized void put(String name) {
            if(!isCreated) {
                isCreated = true;
                this.name = name;
                System.out.println("生产 -> " + name);
                this.notify();
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        public synchronized void get() {
            if(isCreated) {
                System.out.println("消费 ->  " + name);
                isCreated = false;
                notify();
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }
    }

    static class ProductRunnable implements Runnable {
        Product product;

        public ProductRunnable(Product product) {
            this.product = product;
        }

        @Override
        public void run() {
            for (int i = 0; i < 4; i++) {
                product.put("Product " + i);
            }
        }
    }

    static class ConsumeRunnable implements Runnable {
        Product product;

        public ConsumeRunnable(Product product) {
            this.product = product;
        }

        @Override
        public void run() {
            for (int i = 0; i < 4; i++) {
                product.get();
            }
        }
    }

    public static void main(String[] args) {
        Product product = new Product();
        ProductRunnable productRunnable = new ProductRunnable(product);
        ConsumeRunnable consumeRunnable = new ConsumeRunnable(product);
        new Thread(productRunnable).start();
        new Thread(consumeRunnable).start();
    }
}
复制代码

上面是一个简易版的消费者和生产者

输出

生产 -> Product 0
消费 ->  Product 0
生产 -> Product 1
消费 ->  Product 1
生产 -> Product 2
消费 ->  Product 2
生产 -> Product 3
消费 ->  Product 3
复制代码
  • wait() 等待/冻结 :可以将线程冻结,释放CPU执行资格,释放CPU执行权,并把此线程临时存储到线程池,此时对象锁释放了,notify 可以获取到对象锁

  • notify() 唤醒线程池里面 任意一个线程,没有顺序;

  • notifyAll();唤醒线程池里面,全部的线程;

  • 注意wait() notify()这些必须要有同步锁包裹着

到这里,大概也能理解了为什么大多数开源框架内都是使用的notifyAll了吧,notify 只是把CPU执行权释放了,具体哪一个线程被唤醒也不确定,除非开发者非常清楚,否则使用notifyAll 全部唤醒,避免想要唤醒的线程没有被唤醒导致的一系列问题

免责声明:文章版权归原作者所有,其内容与观点不代表Unitimes立场,亦不构成任何投资意见或建议。

android

465

相关文章推荐

未登录头像

暂无评论