进程与线程
进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间
线程:进程中运行的单元,是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行,一个进程至少有一个线程
线程调度
1.分时调度:所有线程轮流使用cpu的使用权,平均分配每个线程占用cpu的时间
2.抢占式调度:优先让优先级高的线程使用cpu,如果优先级相同则随机选择一个,java使用的就是抢占式调度
注意:cpu使用抢占式调度模式在多个线程间进行着高速的切换。对于cpu的一个核心而言,某个时刻只能执行一个线程,而cpu在多个线程间的切换速度相对我们的感觉要快,看上去就是在同一时刻运行。其实多线程并不能提高程序的运行速度,但能够提高运行效率,让cpu的使用率更高。老的说法是并没有真正的多线程,所谓多线程是cpu快速的切换来实现的,但现在有多核,存在真正意义上的多线程。
线程分类
用户线程:是独立存在的,不会因为其他用户线程退出而退出。
守护线程(后台线程):daemon :等待用户线程都执行完了,再自动结束
设置线程为守护线程:Thread.setDaemon(true);
注意:如果要将一个线程设置成守护线程,需要在start之前进行设置,若在start之后设置就没有效果了
异步与同步
异步:同时执行,效率高但是不安全
同步:排队执行,效率低但是安全
并发与并行
并发:指两个或多个事件在同一个时间段内发生
并行:指两个或多个事件在同一时刻发生(同时发生)
创建线程的三种方式
1.继承Thread类,重写run方法
使用步骤:
-
编写类继承Thread类 , 重写run方法 class MyThread extends Thread { @Override public void run() { xxxxxxxxxxxx } }
-
创建MyThread对象 , MyThread thread = new MyThread();
-
通过Thread,启动线程 thread().start();
2.实现Runnable接口,重写run方法
使用步骤:
-
编写类实现Runnable接口 , 实现run方法 class MyTask implements Runnable{ @Override public void run() {xxxxxxxxxxxxxxxxxx } }
-
创建MyTask 对象 , 并传入第一步编写的Callable类对象 MyTask myTask = new MyTask();
-
通过Thread,启动线程 new Thread(myTask ).start();
3.实现Callable接口,重call方法
使用步骤:
-
编写类实现Callable接口 , 实现call方法 class CallableTask implements Callable { @Override public call() throws Exception { return T; } }
-
创建FutureTask对象 , 并传入第一步编写的Callable类对象 FutureTask future = new FutureTask<>(new CallableTask());
-
通过Thread,启动线程 new Thread(future).start();
注意:Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
Runnable和Callable接口的比较
1.都是接口,都可以编写多线程程序,都采用start方法启动线程
2.Runnable没有返回值,Callable可以返回执行的结果
3.Callable接口的cal方法允许抛出异常,Runnable中的run方法不能抛出
Thread和Runnable创建线程的区别
1.继承Thread是单继承,不适合资源共享;Runnable是接口,可同时实现多个接口,且容易实现资源共享
2.实例化线程方式不同:
Thread: new MyThread().start()
Runnable: Runnable myThread = new MyThread(); new Thread(myThread).start()
3.使用Runnable,增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
4.线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
Thread类常用方法
1、start()
启动线程 调用此方法后,系统才会创建线程执行任务,并为之分配相应的资源
2、run()
继承Thread类必须重写run方法,在run方法中定义具体要执行的任务,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务
3、sleep(longmillis)
sleep(longmillis)*//参数为毫秒*
sleep(longmillis,intnanoseconds)*//第一个参数为毫秒,第二个参数为纳秒*
4、yield()
调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程;它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。
注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。
5、join()
join()
join(longmillis)*//**参数为毫秒*
join(longmillis,intnanoseconds)*//**第一个参数为毫秒,第二个参数为纳秒*
假如在main线程中,调用thread.join方法,则main方法会等待thread线程执行完毕或者等待一定的时间。如果调用的是无参join方法,则等待thread执行完毕,如果调用的是指定了时间参数的join方法,则等待一定的时间
6、interrupt()(中断)
以用来中断一个正处于阻塞状态的线程;另外,通过interrupt方法和isInterrupted()方法来停止正在运行的线程。
7、stop()(不安全)
调用stop方法会直接终止run方法的调用,并且会抛出一个ThreadDeath错误,如果线程持有某个对象锁的话,会完全释放锁,导致对象状态不一致。所以stop方法基本是不会被用到的。
8、 setDaemon(boolean on)
将此线程标记为daemon(守护)线程或用户线程。
9、currentThread()
Thread类有一个比较常用的静态方法currentThread()用来获取当前线程。
关系到线程属性的几个方法
1)getId
用来得到线程ID
2)getName和setName
用来得到或者设置线程名称。
3)getPriority和setPriority
用来获取和设置线程优先级。
4)setDaemon和isDaemon
用来设置线程是否成为守护线程和判断线程是否是守护线程。
线程优先级与调度
线程优先级可用数字1-10表示,默认是5
Thread.MIN_PRIORITY=1//最小优先级
Thread.MAX_PRIORITY=10//最大优先级
Thread.NORM_PRIORITY=5//默认优先级
Thread.getPriority()获取线程对象优先级
Thread.setPriority()方法设置优先级
java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程。线程调度器按照线程的优先级决定应调度哪些线程来执行。多数线程的调度时抢先式的 ,即如果在当前线程执行过程中,一个更高优先级的线程进入运行状态,则这个线程立即被调度执行。
线程的状态(生命周期)
创建(new) 就绪(runnable) 运行(running) 阻塞(blocked)
睡眠(timewaiting) 等待(waiting) 消亡(dead)
线程安全问题
情景:火车站几个窗口同时进行卖票,票一共10张
public class ThreadTest {
public static void main(String[] args) {
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
t1.start();
t2.start();
t3.start();
}
}
class Ticket implements Runnable{
int countTicket = 10;
@Override
public void run() {
while(true){
if(countTicket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
countTicket--;
System.out.println(Thread.currentThread().getName() + "出票成功,余票:" + countTicket);
}else{
break;
}
}
}
}
出现了线程安全问题,余票出现了负数
线程安全问题是怎么造成的?
-
多个线程访问共享(资源)变量
-
对操作共享变量的代码有多条(在两个操作代码之间可能有多个线程,就会造成问题)
解决:
1.同步代码块
class Ticket implements Runnable{
int countTicket = 10;
Object o = new Object();
@Override
public void run() {
while(true){
synchronized (o){
if(countTicket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
countTicket--;
System.out.println(Thread.currentThread().getName() + "出票成功,余票:" + countTicket);
}else{
break;
}
}
}
}
}
此处的o为线程安全锁(可以看成钥匙,将代码块看成锁)保证了代码中只有一块执行,处理共享变量的代码是单线程
2.同步方法
class Ticket implements Runnable{
private int countTicket = 10;
Object o = new Object();
@Override
public void run() {
while(true){
boolean flag = sale();
if(!flag){
break;
}
}
}
private synchronized boolean sale(){
if(countTicket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
countTicket--;
System.out.println(Thread.currentThread().getName() + "出票成功,余票:" + countTicket);
return true;
}
return false;
}
}
注意:
1.成员实例方法的锁默认为当前实例(对象)或是this
2.静态方法的锁是class对象
3.显式锁Lock
class Ticket implements Runnable{
private int countTicket = 10;
private Lock lo = new ReentrantLock();//显式锁
@Override
public void run() {
while(true){
lo.lock();
if(countTicket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
countTicket--;
System.out.println(Thread.currentThread().getName() + "出票成功,余票:" + countTicket);
}else{
break;
}
lo.unlock();
}
}
}
显式锁和隐式锁
隐式锁:synchronized修饰的对象或方法就是隐式锁
显式锁:由Lock定义的锁,更符合面向对象,有上锁、解锁的步骤
公平锁和非公平锁
公平锁:线程以抢占式的方式随机获取锁,synchronized的就是公平锁
非公平锁:Lock类锁,按线程申请顺序排队进入
synchronized锁和Lock锁的区别
1.synchronized锁是公平锁,Lock锁是非公平锁
2.synchronized底层依赖于软件层面上的JVM,而Lock类依赖于特殊的CPU指定,可以认为不受JVM的约束,并可以通过其他语言平台来完成底层的实现。在并发量较小的多线程应用程序中,Lock类与synchronized性能相差无几,但在高并发量的条件下,synchronized性能会迅速下降几十倍,而ReentrantLock的性能却能依然维持一个水准,因此我们建议在高并发量情况下使用ReentrantLock。
3.synchronized在发生异常时会自动释放锁,不会发生异常死锁;Lock在发生异常时不会自动释放锁,需要在finally中释放锁
4.Lock锁可以中断,synchronized锁必须等待线程任务执行完在释放锁
死锁
定义:当一个进程申请资源时,如果这时没有可用资源,那么这个进程进入等待状态。有时,如果所申请的资源被其他等待线程占有,那么该等待进程有可能再也无法改变状态,这种情况成为死锁。
死锁的必要条件
1.互斥:至少有一个资源处于非共享模式,即一次只有一个进程可以使用。如果另一个进程申请资源,就必须等待该资源被释放为止。
2.请求和保持:一个线程应占有至少一个资源,并等待另一个资源,而该资源为其他线程所占有,等待该资源被释放,并保持自己的资源不被释放。
3.不剥夺:资源不能被抢占,只能等待被释放。
4.循环等待:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, …, pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, …, n-1),Pn等待的资源被P0占有。
如何避免死锁:
1.加锁顺序(线程按照一定的顺序加锁)
2.加锁时限(线程尝试获取锁的时候加上一定的时限,超时则放弃对该锁的请求,并释放自己占有的资源)
3.死锁检测
线程通信:
void notify();//唤醒在此对象监视器上等待的单个线程。
void notifyAll();//唤醒在此对象监视器上等待的所有线程。
void wait();//导致当前的线程等待,直到其他线程调用此对象的 notify()方法或 notifyAll()方法
void wait(longtimeout);//导致当前的线程等待,直到其他线程调用此对象的 notify()方法或 notifyAll()方法,或者超过指定的时间量。
void wait(longtimeout, int nanos);//导致当前的线程等待,直到其他线程调用此对象的 notify()方法或 notifyAll()方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量
生产者和消费者
public class Demo1 {
public static void main(String[] args) {
Food food = new Food();
new Cook(food).start();
new Waiter(food).start();
}
//厨师
static class Cook extends Thread{
private Food food;
public Cook(Food food){
this.food=food;
}
@Override
public void run(){
for(int i=0;i<100;i++){
if(i%2==0){
food.setNameAndTaste("重庆辣子鸡","麻辣味");
}else{
food.setNameAndTaste("凉拌黄瓜","酸辣味");
}
}
}
}
//服务员
static class Waiter extends Thread{
private Food food;
public Waiter(Food food){
this.food=food;
}
@Override
public void run(){
for(int i=0;i<100;i++){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
food.get();
}
}
}
//饭菜
static class Food{
private String name;
private String taste;
public void setNameAndTaste(String name,String taste){
this.name=name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste=taste;
}
public void get(){
System.out.println("服务员端走的饭菜是:" + this.name + "味道是:" + this.taste);
}
}
}
结果:
饭菜味道出现了错乱
解决:
public class Demo1 {
public static void main(String[] args) {
Food food = new Food();
new Cook(food).start();
new Waiter(food).start();
}
//厨师
static class Cook extends Thread{
private Food food;
public Cook(Food food){
this.food=food;
}
@Override
public void run(){
for(int i=0;i<100;i++){
if(i%2==0){
food.setNameAndTaste("重庆辣子鸡","麻辣味");
}else{
food.setNameAndTaste("凉拌黄瓜","酸辣味");
}
}
}
}
//服务员
static class Waiter extends Thread{
private Food food;
public Waiter(Food food){
this.food=food;
}
@Override
public void run(){
for(int i=0;i<100;i++){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
food.get();
}
}
}
//饭菜
static class Food{
private String name;
private String taste;
private boolean flag = true;
public synchronized void setNameAndTaste(String name,String taste){
if(flag){
this.name=name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste=taste;
flag=false;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void get(){
if(!flag){
System.out.println("服务员端走的饭菜是:" + this.name + " 味道是:" + this.taste);
flag=true;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
结果:
线程池 Executors
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程 就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间. 线程池就是一个容纳多个线程的容 器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。
优势:
1.降低资源消耗
2.提高响应速度
3.提高线程的可管理性
Java中的四种线程池:
1.缓存线程池
/**
* 缓存线程池.
* (长度无限制)
* 执行流程:
* 1. 判断线程池是否存在空闲线程
* 2. 存在则使用
* 3. 不存在,则创建线程 并放入线程池, 然后使用
*/
ExecutorService service = Executors.newCachedThreadPool();
//向线程池中 加入 新的任务
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
2.定长线程池
/**
* 定长线程池.
* (长度是指定的数值)
* 执行流程:3. 单线程线程池
4. 周期性任务定长线程池
* 1. 判断线程池是否存在空闲线程
* 2. 存在则使用
* 3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
* 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
*/
ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
3.单线程线程池
/**
* 效果与定长线程池 创建时传入数值1 效果一致.
* 单线程线程池.
* 执行流程:
* 1. 判断线程池 的那个线程 是否空闲
* 2. 空闲则使用
* 4. 不空闲,则等待 池中的单个线程空闲后 使用
*/
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
4.周期性任务定长线程池
/**
* 周期任务 定长线程池.
* 执行流程:
* 1. 判断线程池是否存在空闲线程
* 2. 存在则使用
* 3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
* 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
*
* 周期性任务执行时:
* 定时执行, 当某个时机触发时, 自动执行某任务 .*/
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
/**
* 定时执行
* 参数1. runnable类型的任务
* 参数2. 时长数字
* 参数3. 时长数字的单位
*/
/*service.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
},5,TimeUnit.SECONDS);
*/
/**
* 周期执行
* 参数1. runnable类型的任务
* 参数2. 时长数字(延迟执行的时长)
* 参数3. 周期时长(每次执行的间隔时间)
* 参数4. 时长数字的单位
*/
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
},5,2, TimeUnit.SECONDS);
}