watson第一篇笔记(线程与进程)
线程与进程原理理解
什么是进程/线程:
- 进程:是指占用一个独立的内存空间,在这块独立空间运行的应用程序。(可以理解为一个软件or一个app,虽然当前市面上许多程序都是多进程的)
- 线程:是进程中的一个执行路径,在进程中,线程可以随意切换,并行,并发运行,一个进程最少有一个线程。线程相当于对进程的多次划分。(可以理解成一个软件中的多项功能,类似于打游戏的同时可以语音,缓存音乐的时候可以播放音乐,)
线程的六大状态:
1.创建(NEW):一般指线程刚刚被创建,还没有调用.start()方法时的状态。
2.运行(RUNNABLE):调用start后,线程被启动运行,这里包括1.ready(准备运行),2.running(正在运行)两种状态。
3.终止(TERMINATED):运行结束,内存回收,线程死亡。
4.阻塞(BLOCKED):线程被阻塞(等待获取锁并运行)。
5.等待(WAITING):无限等待某一线程完成某一动作后再运行。
6.定时等待(TIMED_WAITING):在指定时间等待某一线程完成动作。
同异步和调度
- 同步和异步:
1.1 同步是指程序中各个线程/任务几乎同时加载到内存中的时候,采用排队的方式(先来先运行,或者 高优先级先运行等等)的方式处理。
1.2 异步是指程序中各个线程/任务几乎同时加载到内存中的时候,直接并发执行。
1.3 特点:同步运行可以保证程序安全,但是比较费时。异步运行比较节省时间,但是很有可能导致程序死锁,不安全。 - 调度:
2.1 同步调度:
内存和CPU平均分配时间给各个线程,使得每个线程都能平均使用系统资源
2.2 抢占式调度:
给每个线程建立优先级,优先级高的优先抢占系统资源,遇到同优先级的,系统将随机选取其一运行。
并发与并行
1.并发是指一个时间段内发生的一系列事件
2.并行是指多个事件在同一时间发生。
(并发是你今天吃了饭还睡了觉。并行是你边睡觉边吃饭,最主要区别是对“时间段”的理解)
java实现多线程的方式
1.Thread:
通过继承父类Thread 并重写其run()方法,即可new出一个新线程进行并发执行,是一个线程类。
2.Runnable:
通过接口实现Runnable,成为一个任务类型,通过写其run()方法,可以修改成自己需要的任务。再把任务作为参数传入
代码示例:
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
//Thread
MyThread myThread = new MyThread(); //直接继承一个线程对象,重写其运行任务
//Runnable
MyRunnable myRunnable = new MyRunnable();//创建一个任务对象
Thread t = new Thread(myRunnable); //把任务传给一个新建的线程
/**
* java是抢占式调度的,所以执行顺序是不确定的
*/
myThread.start(); //副1线程启动
t.start(); //副2线程启动
for (int i =2;i<4097;i=i*2){
//主线程启动
System.out.println("主线程:"+i);
}
//匿名内部类实现多线程
new Thread(){
@Override
public void run() {
for (int i =2;i<4097;i=i*2){
System.out.println("副3线程:"+i);//副线3程启动
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
Thread ts = new Thread(new MyRunnables());
ts.start();
ts.sleep(1000);
new Thread(new MyRunnables()).start();
new Thread(new MyRunnables()).start();
}
//任务类Runnable
public static class MyRunnables implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
}
3.二者对比:
* 多线程技术 Thread 和 Runnable的区别
* Thread 是继承的 ,Runnable是接口实现的
* 1. Runnable通过创建任务,导入线程的方式运行
* 2. Thread通过继承线程类,对父类的任务方法重写的方式运行
* Runnable相比Thread有如下好处:
* 1. 多线程执行相同任务时,Runnable效果好
* 2. Thread只能单继承,不能再继承其他类。
* 3. 任务和线程分离,提高健壮性
* 4. 线程池技术接受Runnable任务
4.线程创建的第三种方法(callable)
4.1callable类创建带返回值的线程:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* 线程创建的第三种方法
* callable 带返回值的线程
*/
public class Demo5 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> c = new MyCallable();
FutureTask<Integer> fc = new FutureTask<>(c);
new Thread(fc).start();
//副线程获取返回值的时候,主线程不启动。
int r = fc.get();
if (fc.isDone()){
System.out.println("副线程运行完成");
}
for (int i=1;i<r;i++){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
}
static class MyCallable implements Callable<Integer>{
@Override
public Integer call() throws Exception {
Thread.sleep(100);
for (int i=1;i<10;i++){
Thread.sleep(100);
System.out.println(i);
}
return 30;
}
}
}
注意:可以通过获取callable返回值的方式(new callable.get()),让主线程和副线程分时调度,使其不能并发操作。
总结:各个创建方式都有其特点,在需要线程有返回值的时候使用callable。其他的尽量通过Runnable方式创建线程任务对象的方式实现多线程。
线程不安全和守护线程:
- 线程不安全
1.1线程不安全:简单的理解就是,当多个线程对一个全局变量同时进行读写操作的时候,会造成结果的不一致(A线程在读X变量s时刻的值的时候,B线程在同时(s时刻)进行对X变量修改的操作,这样就产生了一个概率问题,有可能A读取的是X修改前的,也可能是X修改后的数据)这样就产生了线程不安全。
1.2守护线程:守护线程,是保证副线程和主线程同时死亡,当设置setDaemon()为true的时候,会和主线程一起消灭 虽然未执行完。
/**
* 守护线程
*/
public class Demo2 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
//设置副线程为守护线程,当主线程先死亡的时候,(虽然没执行完)自动死亡
t.setDaemon(true);
t.start();
for (int i =2;i<257;i=i*2){
System.out.println("主线程:"+i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class MyRunnable implements Runnable{
@Override
public void run() {
for (int i =2;i<4097;i=i*2){
System.out.println("副线程:"+i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
解决线程不安全——线程锁
-
解决线程不安全的三种方式:
* 1. 使用同步代码块:synchonized(){} * 2. 同步方法:方法前缀加 synchonized * 3. 显式锁 :Lock()
-
代码示例(以模拟抢票为基础):
public class Demo3 {
public static void main(String[] args) {
/**
* 解决线程不安全的方法:
* 1. 使用同步代码块 synchonized(){}
* 2. 同步方法 方法前缀加 synchonized
* 1,2方法都是隐式锁
* 3. 显式锁 Lock()
*/
Runnable t1 = new SellTickets1();
Runnable t2= new SellTickets2();
Runnable t3= new SellTickets3();
new Thread(t,"线程1").start();
new Thread(t,"线程2").start();
new Thread(t,"线程3").start();
new Thread(t,"线程4").start();
}
//同步代码块
static class SellTickets1 implements Runnable{
private int ticket = 10;
//synchronized的锁
private final Object o = new Object();
@Override
public void run() {
while (ticket>0){
synchronized (o){
if (ticket>0){
System.out.println("成功卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket --;
System.out.println(Thread.currentThread().getName()+"剩余票数:"+ticket);
}else {
break;
}
}
}
}
}
//同步方法
static class SellTickets2 implements Runnable{
//使用同步方法使得线程安全
private int ticket = 10;
@Override
public void run() {
while (true){
int flag = sale();
if (flag==0){
break;
}
}
}
public synchronized int sale() {
while (ticket > 0) {
if (ticket > 0) {
System.out.println("成功卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName() + "剩余票数:" + ticket);
return 1;
}
return 0;
}
return 0;
}
}
//显式锁
static class SellTickets3 implements Runnable{
private int ticket = 10;
Lock lock = new ReentrantLock();
@Override
public void run() {
/**
* 通过不断的开锁解锁达到控制一个线程运行的目的
*/
while (ticket>0){
//锁住 只有一个线程能进入下面的代码
lock.lock();
if (ticket>0){
System.out.println("成功卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket --;
System.out.println(Thread.currentThread().getName()+"剩余票数:"+ticket);
}else {
lock.unlock();
break;
}
//解锁,上面的代码继续运行
lock.unlock();
}
}
}
}
同步代码块:
是将会产生数据修改的代码片段添加一个Synchonized(object){}代码块,并赋予一个object类型的锁,这个锁要和所有线程所操作的共同变量放在一起,当进行中括号内代码运行的时候,会对数据进行上锁,期间任何线程都不可以对该共同变量进行读写操作。
同步方法:
在方法体外加上synchronized标记,使得一次只能被一个线程调用方法,原理大致和同步代码块一致。
显式锁:
在即将发生数据修改的代码前添加Lock.lock()方法,使得只能单线程运行以下代码,在代码结束阶段进行Lock.unlock()方法,使得线程重新竞争运行。
显式锁(Lock)和隐式锁(Sync)的区别(面试大概率会问)
1. 出身问题:
Sync是JVM层面的,调用系统底层的monitorenter对象加载。
Lock是API层面的,自己本身就是一个类,调用时需要实例化。
2. 同步锁问题:
Sync会自动释放锁,不会产生死锁。
Lock是手动释放锁,操作不当会导致线程死锁。
3. 中断处理:
Sync运行时不能被中断。
Lock可以调用方法(interrupt())中断或挂起线程。
4. 公平机制:
Sync是非公平锁,抢占式调度。
Lock默认是非公平锁,当实例化时可以传入”true“ 变为公平锁。
5. 唤醒机制:
Sync要么随机唤醒一个进程,要么全部唤醒进程。
Lock可以做到精确唤醒和分组唤醒。
6. 性能对比:
JDK1.5之前,Sync性能比Lock差。
JDK1.6之后,Sync性能大幅提升,性能和Lock相差无几,且还有提升空间。
且官方较支持使用Sync进行多线程。
线程池
顾名思义,线程池就是一个容纳多个线程的容器,通过建立线程池,就不会使得一个线程被反复创建销毁,减轻内存负担的同时,在多任务的时候也能提高运行效率。
执行流程是:线程创建->任务创建->执行任务->关闭线程
线程池类型:
1.缓存线程池:
缓存线程池是建立一个线程池,通过判断任务数量是否多于当前线程数,如果任务多则添加线程。
在代码中,创建了一个缓存线程池,初始线程容量为0,当同时传入3个任务后,自动创建3个线程添加到线程池中,所以结果是三个任务同时运行
public static void main(String[] args) {
//创建缓存线程池
ExecutorService service = Executors.newCachedThreadPool();
Runnable r1 = new SellTickets1();
Runnable r2 = new SellTickets2();
Runnable r3 = new SellTickets3();
service.execute(r1);
service.execute(r2);
service.execute(r3);
}
2.定长线程池:
定长线程池的原理是,创建一个指定长度的线程池,当任务少于当前线程数量的时候,部分线程处于休眠状态,当任务数大于当前线程数的时候,部分任务需进行等待线程完成任务。
public static void main(String[] args) {
//创建了一个有两个线程的定长线程池,传入3个任务
//必然有一个任务需要排队
ExecutorService service = Executors.newFixedThreadPool(2);
Runnable r1 = new SellTickets1();
Runnable r2 = new SellTickets2();
Runnable r3 = new SellTickets3();
service.execute(r1);
service.execute(r2);
service.execute(r3);
}
3.单线线程池:其实就是定长线程池的参数是1(只有一个线程的线程池)
4.周期性任务定长线程池:
在定长线程池的基础上,给予一个时间周期,当到达该周期的时候才执行该任务:
public static void main(String[] args) {
//创建了一个有1个线程的周期性定长线程池,传入3个任务
ScheduledExecutorService service = Executors.newScheduledThreadPool(1);
Runnable r1 = new SellTickets1();
Runnable r2 = new SellTickets2();
Runnable r3 = new SellTickets3();
//r1任务延迟20秒执行
service.schedule(r1,20,TimeUnit.SECONDS);
service.execute(r2);
service.execute(r3);
}