进程和线程是什么?
-
进程:在我们的电脑上有很多单独运行的程序,这每一个程序都有一个独立的进程,进程之间相互独立,比如我们电脑上的qq,微信,电脑管家等。
-
线程:进程想执行任务就需要依赖线程,一个进程中至少有一个线程。那么什么是多线程呢?先来了解一下串行和并行。
- 串行:就是一个线程执行执行多条任务时,必须先执行完A,然后再执行B,最后执行C。
- 并行:比如在下载多个文件时开启多条线程,多个文件同时进行下载,这里是严格意义上的,在同一时刻发生的,并行在时间上是重叠的。
那么再针对多线程来说,就好比我们使用训练下载文件的时候可以同时下载多个文件,按照单线程来讲就必须下载完一个文件后再开始下载另一个文件。
线程的生命周期
- 新建 :从新建一个线程对象到程序start() 这个线程之间的状态,都是新建状态;
- 就绪 :线程对象调用start()方法后,就处于就绪状态,等到JVM里的线程调度器的调度;
- 运行 :就绪状态下的线程在获取CPU资源后就可以执行run(),此时的线程便处于运行状态,运行状态的线程可变为就绪、阻塞及死亡三种状态。
- 等待/阻塞/睡眠 :在一个线程执行了sleep(睡眠)、suspend(挂起)等方法后会失去所占有的资源,从而进入阻塞状态,在睡眠结束后可重新进入就绪状态。
- 终止 :run()方法完成后或发生其他终止条件时就会切换到终止状态。
创建线程的多种方式
1、继承Thread类:
- 定义类继承Thread
- 复写Thread类中的run方法;
目的:将自定义代码存储在run方法,让线程运行 - 调用线程的start方法:
该方法有两步:启动线程,调用run方法。
public class ThreadDemo1 {
public static void main(String[] args) {
//创建两个线程
ThreadDemo td = new ThreadDemo("zhangsan");
ThreadDemo tt = new ThreadDemo("lisi");
//执行多线程特有方法,如果使用td.run();也会执行,但会以单线程方式执行。
td.start();
tt.start();
//主线程
for (int i = 0; i < 5; i++) {
System.out.println("main" + ":run" + i);
}
}
}
//继承Thread类
class ThreadDemo extends Thread{
//设置线程名称
ThreadDemo(String name){
super(name);
}
//重写run方法。
public void run(){
for(int i = 0; i < 5; i++){
System.out.println(this.getName() + ":run" + i); //currentThread() 获取当前线程对象(静态)。 getName() 获取线程名称。
}
}
}
2、实现Runnable接口: 接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为run 的无参方法。
- 定义类实现Runnable接口
- 覆盖Runnable接口中的run方法
将线程要运行的代码放在该run方法中。 - 通过Thread类建立线程对象。
- 将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
自定义的run方法所属的对象是Runnable接口的子类对象。所以要让线程执行指定对象的run方法就要先明确run方法所属对象 - 调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。
public class RunnableDemo {
public static void main(String[] args) {
RunTest rt = new RunTest();
//建立线程对象
Thread t1 = new Thread(rt);
Thread t2 = new Thread(rt);
//开启线程并调用run方法。
t1.start();
t2.start();
}
}
//定义类实现Runnable接口
class RunTest implements Runnable{
private int tick = 10;
//覆盖Runnable接口中的run方法,并将线程要运行的代码放在该run方法中。
public void run(){
while (true) {
if(tick > 0){
System.out.println(Thread.currentThread().getName() + "..." + tick--);
}
}
}
}
3、通过Callable和Future创建线程:
- 创建Callable接口的实现类,并实现call()方法,改方法将作为线程执行体,且具有返回值。
- 创建Callable实现类的实例,使用FutrueTask类进行包装Callable对象,FutureTask对象封装了Callable对象的call()方法的返回值
- 使用FutureTask对象作为Thread对象启动新线程。
- 调用FutureTask对象的get()方法获取子线程执行结束后的返回值。
public class CallableFutrueTest {
public static void main(String[] args) {
CallableTest ct = new CallableTest(); //创建对象
FutureTask<Integer> ft = new FutureTask<Integer>(ct); //使用FutureTask包装CallableTest对象
for(int i = 0; i < 100; i++){
//输出主线程
System.out.println(Thread.currentThread().getName() + "主线程的i为:" + i);
//当主线程执行第30次之后开启子线程
if(i == 30){
Thread td = new Thread(ft,"子线程");
td.start();
}
}
//获取并输出子线程call()方法的返回值
try {
System.out.println("子线程的返回值为" + ft.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class CallableTest implements Callable<Integer>{
//复写call() 方法,call()方法具有返回值
public Integer call() throws Exception {
int i = 0;
for( ; i<100; i++){
System.out.println(Thread.currentThread().getName() + "的变量值为:" + i);
}
return i;
}
}
4、使用线程池创建:
public class ThreadTest {
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(50);
public void dothing(){
for(int i=0;i<50;i++){
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
//do something
}
});
}
fixedThreadPool.shutdown();//关闭线程池
//此处不可以删除或注释,需要线程执行结束后再执行别的内容,即只有线程结束后才会继续向下执行
while (!fixedThreadPool.isTerminated()) {
}
}
}
- 线程池的特点:
1.通过复用已存在的线程,降低线程创建和消耗带来的损耗。
2.提高相应速度,通过复用已存在的新线程,无序等待便能立即执行
3.提供更强大的功能:延时定时线程池
1.可缓存线程池 Executors.newCacheThreadPool():先查看池中有没有线程。如果有直接使用,没有则新建。(通常适用于执行一些生存期很短的线程)
线程池为无限大,当执行完上一个任务后,后面的任务会复用上一个任务的线程,而不用每次新建。
2.定长线程池Executors.newFixedThreadPool(int n):创建一个固定长度的线程池,以共享的无界队列方式来运行这些线程。
3.延时定时线程池Executors.newScheduledThreadPool(int n):创建一个定长线程池,支持延时和定时。
4.单线程化的线程池Executors.newSingleThreadExecutor():它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
线程安全和线程锁
1、线程安全
既然提到了线程安全,我们就知道有的线程肯定是不安全的,那么来举两个例子,有如下代码:
Integer count = 0;
public void getCount() {
count ++;
System.out.println(count);
}
当我们开启线程并且每个线程访问10次时,有如下结果:
这里出现了两个重复数字,可以看出这个线程并不是安全的,其中最常见的问题就是A线程进入后读取count还没开始加一时,线程B进来了,这样导致两个线程拿到的值时一样的。
再看下面这段代码:
public void threadMethod(int i) {
i = i + 1;
}
这段代码很容易看出来,这个方法只对传入的参数进行操作,对别的线程参数没有不会有任何影响,所以说它时绝对安全的。
所以可以总结一下:当多个线程访问某个方法时,不管你通过怎样的调用方式、或者说这些线程如何交替地执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类是线程安全的。
2、线程锁
既然存在线程安全的问题,那么肯定得想办法解决这个问题,怎么解决?我们说说常见的几种方式
- synchronized关键字
关键字synchronized的作用是实现线程间的同步。它的工作是对同步的代码加锁,使得每一次,只能有一个线程进入同步块,从而保证线程间的安全性。
关键字synchronized可以有多张用法,这里做一个简单的整理:
- 指定加锁对象:对给定对象加锁,进入同步代码前要获取给定对象的锁。
- 直接作用于实例方法:相当于给当前实例加锁,进入同步代码块前要获得当前实例的锁。
- 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获取当前类的锁。
下面来分别说一下上面的三点:
假设我们有下面这样一个Runnable,在run方法里对静态成员变量sCount自增10000次:
class Count implements Runnable {
private static int sCount = 0;
public static int getCount() {
return sCount;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
sCount++;
}
}
}
假设我们在两个Thread里面同时跑这个Runnable:
Count count = new Count();
Thread t1 = new Thread(count);
Thread t2 = new Thread(count);
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(Count.getCount());
得到的结果并不是20000,而是一个比20000小的数,如14233。
这是为什么呢?假设两个线程分别读取sCount为0,然后各自技术得到sCount为1,并先后写入这个结果,因此,虽然sCount++执行了2次,但是实际sCount的值只增加了1。
我们可以用指定加锁对象的方法解决这个问题,这里因为两个Thread跑的是同一个Count实例,所以可以直接给this加锁:
class Count implements Runnable {
private static int sCount = 0;
public static int getCount() {
return sCount;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
synchronized (this) {
sCount++;
}
}
}
}
synchronized直接作用于静态方法的用法和上面的给实例方法加锁类似,不过它是作用于静态方法:
class Count implements Runnable {
private static int sCount = 0;
public static int getCount() {
return sCount;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increase();
}
}
private static synchronized void increase() {
sCount++;
}
}
- 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中的代码无论如何都是会执行的。
写个主方法,开启两个线程测试一下我们的程序是否正常:
public static void main(String[] args) {
LockTest lockTest = new LockTest();
// 线程1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// Thread.currentThread() 返回当前线程的引用
lockTest.method(Thread.currentThread());
}
}, "t1");
// 线程2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
lockTest.method(Thread.currentThread());
}
}, "t2");
t1.start();
t2.start();
}
结果:
可以看出我们的执行,是没有任何问题的。
还有其他很多锁就不一一列举,详细介绍可以看下面这篇博客:https://www.jianshu.com/p/fa084227c96b