本文会介绍Java中多线程与并发的基础,适合初学者食用。
在计算机发展初期,每台计算机是串行地执行任务的,如果碰上需要IO的地方,还需要等待长时间的用户IO,后来经过一段时间有了批处理计算机,其可以批量串行地处理用户指令,但本质还是串行,还是不能并发执行。
如何解决并发执行的问题呢?于是引入了进程的概念,每个进程独占一份内存空间,进程是内存分配的最小单位,相互间运行互不干扰且可以相互切换,现在我们所看到的多个进程“同时"在运行,实际上是进程高速切换的效果。
那么有了线程之后,我们的计算机系统看似已经很完美了,为什么还要进入线程呢?如果一个进程有多个子任务,往往一个进程需要逐个去执行这些子任务,但往往这些子任务是不相互依赖的,可以并发执行,所以需要CPU进行更细粒度的切换。所以就引入了线程的概念,线程隶属于某一个进程,它共享进程的内存资源,相互间切换更快速。
进程与线程的区别:
Java中进程与线程的关系:
Java中创建线程的方式有两种,不管使用继承Thread的方式还是实现Runnable接口的方式,都需要重写run方法。调用start方法会创建一个新的线程并启动,run方法只是启动线程后的回调函数,如果调用run方法,那么执行run方法的线程不会是新创建的线程,而如果使用start方法,那么执行run方法的线程就是我们刚刚启动的那个线程。
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new SubThread());
thread.run();
thread.start();
}
}
class SubThread implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("执行本方法的线程:"+Thread.currentThread().getName());
}
}
区别
通过上述源码图,不难看出,Thread是一个类,而Runnable是一个接口,Runnable接口中只有一个没有实现的run方法,可以得知,Runnable并不能独立开启一个线程,而是依赖Thread类去创建线程,执行自己的run方法,去执行相应的业务逻辑,才能让这个类具备多线程的特性。
使用继承Thread类方式创建子线程
public class Main extends Thread{
public static void main(String[] args) {
Main main = new Main();
main.start();
}
@Override
public void run() {
System.out.println("通过继承Thread接口方式创建子线程成功,当前线程名:"+Thread.currentThread().getName());
}
}
运行结果:
使用实现Runnable接口方式创建子线程
public class Main{
public static void main(String[] args) {
SubThread subThread = new SubThread();
Thread thread = new Thread(subThread);
thread.start();
}
}
class SubThread implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("通过实现Runnable接口创建子线程成功,当前线程名:"+Thread.currentThread().getName());
}
}
运行结果:
使用匿名内部类方式创建子线程
public class Main{
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("使用匿名内部类方式创建线程成功,当前线程名:"+Thread.currentThread().getName());
}
});
thread.start();
}
}
运行结果:
通过刚才的学习,我们知道多线程的逻辑需要放到run方法中去执行,而run方法是没有返回值的,那么遇到需要返回值的状况就不好解决,那么如何实现子线程返回值呢?
通过让主线程等待,直到子线程运行完毕为止。
实现方式:
public class Main{
static String str;
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
str="子线程执行完毕";
}
});
thread.start();
//如果子线程还未对str进行赋值,则一直轮转
while(str==null) {}
System.out.println(str);
}
}
join()方法可以阻塞当前线程以等待子线程处理完毕。
实现方式:
public class Main{
static String str;
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
str="子线程执行完毕";
}
});
thread.start();
//如果子线程还未对str进行赋值,则一直轮转
try {
thread.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(str);
}
}
join方法能做到比主线程等待法更精准的控制,但是join方法的控制粒度并不够细。比如,我需要控制子线程将字符串赋一个特定的值时,再执行主线程,这种操作join方法是没有办法做到的。
在JDK1.5之前,线程是没有返回值的,通常程序猿需要获取子线程返回值颇费周折,现在Java有了自己的返回值线程,即实现了Callable接口的线程,执行了实现Callable接口的线程之后,可以获得一个Future对象,在该对象上调用一个get方法,就可以执行子线程的逻辑并获取返回的Object。
实现方式1(错误示例):
public class Main implements Callable<String>{
@Override
public String call() throws Exception {
// TODO Auto-generated method stub
String str = "我是带返回值的子线程";
return str;
}
public static void main(String[] args) {
Main main = new Main();
try {
String str = main.call();
System.out.println(str);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
运行结果:
实现方式2(使用FutureTask):
public class Main implements Callable<String>{
@Override
public String call() throws Exception {
// TODO Auto-generated method stub
String str = "我是带返回值的子线程";
return str;
}
public static void main(String[] args) {
FutureTask<String> task = new FutureTask<String>(new Main());
new Thread(task).start();
try {
if(!task.isDone()) {
System.out.println("任务没有执行完成");
}
System.out.println("等待中...");
Thread.sleep(3000);
System.out.println(task.get());
} catch (InterruptedException | ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
运行结果:
实现方法3(使用线程池配合Future获取):
public class Main implements Callable<String>{
@Override
public String call() throws Exception {
// TODO Auto-generated method stub
String str = "我是带返回值的子线程";
return str;
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService newCacheThreadPool = Executors.newCachedThreadPool();
Future<String> future = newCacheThreadPool.submit(new Main());
if(!future.isDone()) {
System.out.println("线程尚未执行结束");
}
System.out.println("等待中");
Thread.sleep(300);
System.out.println(future.get());
newCacheThreadPool.shutdown();
}
}
运行结果:
Java线程主要分为以下六个状态:新建态(new),运行态(Runnable),无限期等待(Waiting),限期等待(TimeWaiting),阻塞态(Blocked),结束(Terminated)。
新建态是线程处于已被创建但没有被启动的状态,在该状态下的线程只是被创建出来了,但并没有开始执行其内部逻辑。
运行态分为Ready和Running,当线程调用start方法后,并不会立即执行,而是去争夺CPU,当线程没有开始执行时,其状态就是Ready,而当线程获取CPU时间片后,从Ready态转为Running态。
处于等待状态的线程不会自动苏醒,而只有等待被其它线程唤醒,在等待状态中该线程不会被CPU分配时间,将一直被阻塞。以下操作会造成线程的等待:
锁:https://juejin.im/post/5d8da403f265da5b5d203bf4
处于限期等待的线程,CPU同样不会分配时间片,但存在于限期等待的线程无需被其它线程显式唤醒,而是在等待时间结束后,系统自动唤醒。以下操作会造成线程限时等待:
当多个线程进入同一块共享区域时,例如Synchronized块、ReentrantLock控制的区域等,会去整夺锁,成功获取锁的线程继续往下执行,而没有获取锁的线程将进入阻塞状态,等待获取锁。
已终止线程的线程状态,线程已结束执行。
Sleep和Wait者两个方法都可以使线程进入限期等待的状态,那么这两个方法有什么区别呢?
测试代码:
public class Main{
public static void main(String[] args) {
Thread threadA = new Thread(new ThreadA());
Thread threadB = new Thread(new ThreadB());
threadA.setName("threadA");
threadB.setName("threadB");
threadA.start();
threadB.start();
}
public static synchronized void print() {
System.out.println("当前线程:"+Thread.currentThread().getName()+"执行Sleep");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("当前线程:"+Thread.currentThread().getName()+"执行Wait");
try {
Main.class.wait(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("当前线程:"+Thread.currentThread().getName()+"执行完毕");
}
}
class ThreadA implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
Main.print();
}
}
class ThreadB implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
Main.print();
}
}
执行结果:
从上面的结果可以分析出:当线程A执行sleep后,等待一秒被唤醒后继续持有锁,执行之后的代码,而执行wait之后,立即释放了锁,不仅让出了CPU还让出了锁,而后线程B立即持有锁开始执行,和线程A执行了同样的步骤,当线程B执行wait方法之后,释放锁,然后线程A拿到锁打印了第一个执行完毕,然后线程B打印执行完毕。
notify可以唤醒一个处于等待状态的线程,上代码:
public class Main{
public static void main(String[] args) {
Object lock = new Object();
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
print();
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
print();
lock.notify();
}
}
});
threadA.setName("threadA");
threadB.setName("threadB");
threadA.start();
threadB.start();
}
public static void print() {
System.out.println("当前线程:"+Thread.currentThread().getName()+"执行print");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("当前线程:"+Thread.currentThread().getName()+"执行完毕");
}
}
执行结果:
代码解释:线程A在开始执行时立即调用wait进入无限等待状态,如果没有别的线程来唤醒它,它将一直等待下去,所以此时B持有锁开始执行,并且在执行完毕时调用了notify方法,该方法可以唤醒wait状态的A线程,于是A线程苏醒,开始执行剩下的代码。
notifyAll可以用于唤醒所有等待的线程,使所有处于等待状态的线程都变为ready状态,去重新争夺锁。
public class Main{
public static void main(String[] args) {
Object lock = new Object();
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
print();
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
print();
lock.notifyAll();
}
}
});
threadA.setName("threadA");
threadB.setName("threadB");
threadA.start();
threadB.start();
}
public static void print() {
System.out.println("当前线程:"+Thread.currentThread().getName()+"执行print");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("当前线程:"+Thread.currentThread().getName()+"执行完毕");
}
}
执行结果:
要唤醒前一个例子中的线程A,不光notify方法可以做到,调用notifyAll方法同样也可以做到,那么两者有什么区别呢?
要说清楚他们的区别,首先要简单的说一下Java synchronized的一些原理,在openjdk中查看java的源码可以看到,java对象中存在monitor锁,monitor对象中包含锁池和等待池。
锁池,假设有多个对象进入synchronized块争夺锁,而此时已经有一个对象获取到了锁,那么剩余争夺锁的对象将直接进入锁池中。
等待池,假设某个线程调用了对象的wait方法,那么这个线程将直接进入等待池,而等待池中的对象不会去争夺锁,而是等待被唤醒。
下面可以说notify和notifyAll的区别了:
notifyAll会让所有处于等待池中的线程全部进入锁池去争夺锁,而notify只会随机让其中一个线程去争夺锁。
/**
* A hint to the scheduler that the current thread is willing to yield
* its current use of a processor. The scheduler is free to ignore this
* hint.
*
* <p> Yield is a heuristic attempt to improve relative progression
* between threads that would otherwise over-utilise a CPU. Its use
* should be combined with detailed profiling and benchmarking to
* ensure that it actually has the desired effect.
*
* <p> It is rarely appropriate to use this method. It may be useful
* for debugging or testing purposes, where it may help to reproduce
* bugs due to race conditions. It may also be useful when designing
* concurrency control constructs such as the ones in the
* {@link java.util.concurrent.locks} package.
*/
public static native void yield();
yield源码上有一段长长的注释,其大意是说:当前线程调用yield方法时,会给当前线程调度器一个暗示,当前线程愿意让出CPU的使用,但是它的作用应结合详细的分析和测试来确保已经达到了预期的效果,因为调度器可能会无视这个暗示,使用这个方法是不那么合适的,或许在测试环境中使用它会比较好。
测试:
public class Main{
public static void main(String[] args) {
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("ThreadA正在执行yield");
Thread.yield();
System.out.println("ThreadA执行yield方法完成");
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("ThreadB正在执行yield");
Thread.yield();
System.out.println("ThreadB执行yield方法完成");
}
});
threadA.setName("threadA");
threadB.setName("threadB");
threadA.start();
threadB.start();
}
测试结果:
可以看出,存在不同的测试结果,这里选出两张。
第一种结果:线程A执行完yield方法,让出cpu给线程B执行。然后两个线程继续执行剩下的代码。
第二种结果:线程A执行yield方法,让出cpu给线程B执行,但是线程B执行yield方法后并没有让出cpu,而是继续往下执行,此时就是系统无视了这个暗示。
interrupt函数可以中断一个线程,在interrupt之前,通常使用stop方法来终止一个线程,但是stop方法过于暴力,它的特点是,不论被中断的线程之前处于一个什么样的状态,都无条件中断,这会导致被中断的线程后续的一些清理工作无法顺利完成,引发一些不必要的异常和隐患,还有可能引发数据不同步的问题。
interrupt方法的原理与stop方法相比就显得温柔的多,当调用interrupt方法去终止一个线程时,它并不会暴力地强制终止线程,而是通知这个线程应该要被中断了,和yield一样,这也是一种暗示,至于是否应该中断,由被中断的线程自己去决定。当对一个线程调用interrupt方法时:
线程池的引入是用来解决在日常开发的多线程开发中,如果开发者需要使用到非常多的线程,那么这些线程在被频繁的创建和销毁时,会对系统造成一定的影响,有可能系统在创建和销毁这些线程所耗费的时间会比完成实际需求的时间还要长。
另外,在线程很多的状况下,对线程的管理就形成了一个很大的问题,开发者通常要将注意力从功能上转移到对杂乱无章的线程进行管理上,这项动作实际上是非常耗费精力的。
newFixThreadPool(int nThreads)
指定工作线程数量的线程池。
newCachedThreadPool()
处理大量中断事件工作任务的线程池,
newSingleThreadExecutor()
创建唯一的工作线程来执行任务,如果线程异常结束,会有另一个线程取代它。可保证顺序执行任务。
newSingleThreadScheduledExecutor()与newScheduledThreadPool(int corePoolSize)
定时或周期性工作调度,两者的区别在于前者是单一工作线程,后者是多线程
newWorkStealingPool()
内部构建ForkJoinPool,利用working-stealing算法,并行地处理任务,不保证处理顺序。
Fork/Join框架:把大任务分割称若干个小任务并行执行,最终汇总每个小任务后得到大任务结果的框架。
线程是稀缺资源,如果无限制地创建线程,会消耗系统资源,而线程池可以代替开发者管理线程,一个线程在结束运行后,不会销毁线程,而是将线程归还线程池,由线程池再进行管理,这样就可以对线程进行复用。
所以线程池不但可以降低资源的消耗,还可以提高线程的可管理性。
public class Main{
public static void main(String[] args) {
ExecutorService newFixThreadPool = Executors.newFixedThreadPool(10);
newFixThreadPool.execute(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("通过线程池启动线程成功");
}
});
newFixThreadPool.shutdown();
}
}
要知道这个点首先要先说说ThreadPoolExecutor的构造函数,其中有几个参数:
那么新任务提交后会执行下列判断:
这个问题并不是什么秘密,在网上各大技术网站均有文章说明,我就拿一个最受认可的写上吧
当然这个也不能完全依赖这个公式,更多的是要依赖平时的经验来操作,这个公式也只是仅供参考而已。
本文提供了一些Java多线程和并发方面最最基础的知识,适合初学者了解Java多线程的一些基本知识
网站声明:如果转载,请联系本站管理员。否则一切后果自行承担。
添加我为好友,拉您入交流群!
请使用微信扫一扫!