本文为原创文章,转载请注明出处
查看[Java]系列内容请点击:https://www.jianshu.com/nb/45938443
1.线程与进程的区别
简单理解,线程比进程更加轻量级,一个进程可以包含多个线程,实际上,甚至可以认为在Java程序运行的过程中,一个JVM只启动一个JVM进程,其他的程序运行都是以线程的方式存在。
进程之间无法共享变量,进程之间共享数据往往通过第三方的文件、数据库等进行共享;线程之间可以互相共享变量。
由于线程之间会互相共享变量,所以会出现变量的使用存在冲突的问题,使用不当的时候会造成一定程度上的死锁现象。
2.线程数量设置为多少合适?
这个一般是要看我们CPU的数量以及线程的类型,一般来说,线程的数量不应该比CPU数量多太多,太多没有意义,反而会造成CPU的频繁切换带来更大的开销。
而对于线程类型,我们主要分为CPU密集型(即计算密集型)和I/O密集型。
2.1.CPU密集型线程
对于CPU密集型线程,一般经验值设置线程数量为:
Z = N × ( X + Y ) ÷ X , 其中,N=CPU核数,X=单个线程需要的计算时间,Y=线程需要等待的时间
比如,对于4核CPU,单个线程计算时间为10,等待时间为4,那么Z=5.6
,所以应该将线程数设置为6
2.2.I/O密集型线程
这类任务一般需要根据不同的I/O任务来进行设置,比如如果对于需要频繁随机读取机械硬盘数据的线程,线程数一般不宜设置过大,因为过大会导致系统的磁盘寻道时间过多,降低性能。如果是对于访问远程数据等,可以在网络带宽允许的情况下多开几个线程,从而实现并发访问。
3.线程的状态
线程具有6种状态,线程可以在不同的状态之间切换,分别如下:
- 1、
New
新建状态:当我们创建了一个线程,线程还未开始的时候是新建状态。- 2、
Runnable
可运行状态:当调用线程的start()
方法以后,线程就处于可运行状态,注意,可运行状态的线程可能在运行也可能不在运行,主要需要看有没有获得CPU的时间片。- 3、
Blocked
阻塞状态:线程等待获得线程锁,就处于这个状态,获得锁后重新回到Runnable
状态。- 4、
Waiting
等待状态:等待线程被通知的状态,比如调用了Thread.join
或者Object.wait()
等方法,都会等待某个事件结束,然后给当前线程发通知,唤醒当前线程,当前线程被唤醒后处于Runnable
状态。- 5、
Timed waiting
计时等待状态:调用了Thread.sleep()
的线程处于计时等待状态,等待计时结束,结束后可以转回Runnable
状态。- 6、
Terminated
终止状态:终止状态,线程终止。
还有一点需要说明的是,调用Thread.yield()
方法可以让当前线程让出运行权,但是一般让出后也不能保证其他线程能继续运行,也完全有可能是当前线程继续运行,所以这个方法一般不用。
还有,在早期的Java版本中可以强制终止一个线程,现在已经被废除。
可以调用线程的interrupt
方法来请求终止线程,但是线程是否响应请求是由线程内部决定的,而且这个方法会抛出异常,所以在实际使用的过程中,自己定义一个线程标志位,自己通过其他方式请求中断线程而不使用interrupt
方法会让代码可读性更高一些。所以interrupt
方法在实际使用过程中也尽量不要使用。
4.线程的底层同步机制
假设大家都已经清楚了线程的资源竞争的概念,如果有不清楚,请看我以前的一些文章:https://www.jianshu.com/p/55a13cf06194
在介绍我们常用的synchronized
关键字之前,我们先来介绍其底层的实现原理。
4.1.重入锁和读写锁
对于需要竞争的资源,底层使用JVM的锁来进行资源的锁定,在java.util.concurrent.locks
包中有一系列关于锁的类:
Lock lock = new ReentrantLock(); // 如果是对象级别的锁,lock就需要定义成对象的字段
lock.lock(); // 从这里开始,开始加互斥锁,其他企图获得lock锁的程序被迫转入【阻塞】状态
// do sth ... 临界区代码
lock.unlock(); // 锁释放,允许其他线程继续获得锁,让其他线程继续竞争锁
除了ReentrantLock
锁以外,还有好几种其他的锁:
ReentrantLock implements Lock
:重入锁,具有持有锁计数,被锁住的代码可以调用其他的具有相同锁的代码。ReentrantReadWriteLock implements ReadWriteLock
:读写锁,读写分别各一个锁,可实现读锁和写锁互不影响,具体的读锁和写锁依然是Lock
实现。
4.2.条件变量(条件对象)
当线程进入到某个临界区之后,发现当前的条件不满足,一般来说处理办法是直接释放锁然后结束线程,但是如果要求线程继续等待,等待到满足条件再执行的话,就需要使用条件变量来实现了:
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
while(!YourCondition) condition.await(); // 从调用await开始该线程转入【阻塞】状态,并等待其他线程调用signalAll()
// do sth ... 临界区代码
condition.signalAll(); // 唤醒其他所有等待该条件的线程,重新竞争lock锁,并通过while循环重新检测条件
lock.unlock(); // 锁释放,允许其他线程继续获得锁,让其他线程继续竞争锁
如上面注释所说,while
循环用来检测条件,条件不满足就会自动阻塞,并等待其他线程唤醒,唤醒后重新获得锁并重新检测条件,如此往复。
当没有线程唤醒阻塞的线程时,就会出现死锁现象。
5.synchronized
关键字
为了避免手动加锁解锁的过程,Java提供了synchronized
关键字来实现自动加锁。synchronized
关键字可以加在一段代码块上、一个方法上等。Java从最底层的Object
类开始,就内置了一个默认的锁(通过JNI实现),根据关键字位置加的不同,所获取的锁也不同:
synchronized(object){...} // 获取object对象的锁,持有该锁的所有代码都不能同时执行
public synchronized void f(){...} // 获取该对象的锁,该对象内所有拥有该锁的代码都不能同时执行
public static synchronized void f(){...} // 获取该类的Class对象锁,所有具有该所的代码不能同时执行
例如,我们看下面代码:
public class MainTest {
public static void main(String[] args) throws Exception {
MainTest test = new MainTest();
// test1与test2持有的是test对象锁,所以test1获取锁期间,test2不能执行
new Thread(test::test1).start();
new Thread(test::test2).start();
// staticTest1与staticTest2持有的是Class<MainTest>对象锁,所以staticTest1获取锁期间,staticTest2不能执行
new Thread(MainTest::staticTest1).start();
new Thread(MainTest::staticTest2).start();
}
public synchronized void test1() {
for (int i = 0; i < 5; i++) {
System.out.println("test1");
sleep(1000);
}
}
public synchronized void test2() {
for (int i = 0; i < 5; i++) {
System.out.println("test2");
sleep(1000);
}
}
public static synchronized void staticTest1() {
for (int i = 0; i < 5; i++) {
System.out.println("staticTest1");
sleep(1000);
}
}
public static synchronized void staticTest2() {
for (int i = 0; i < 5; i++) {
System.out.println("staticTest2");
sleep(1000);
}
}
public static void sleep(long time) {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出结果:
test1
staticTest1
test1
staticTest1
test1
staticTest1
staticTest1
test1
test1
staticTest1
test2
staticTest2
staticTest2
test2
staticTest2
test2
staticTest2
test2
staticTest2
test2
知道了以上内容之后,请自主实现一个需求:
已知一段代码的运行时间很长,所需耗费的CPU非常大,如果线程数量过多的话,会造成电脑卡机等问题,现在想让这段代码最多只允许
x
个线程运行,即并发量最高为x
,如何实现呢?