java基础之线程
文章目录
基本概念
进程:一个程序对应一个进程
线程:一个进程里可以有多个线程,多个线程可并发执行。
线程的创建
1.继承Thread类并重写run()方法
使用时使用 new A().start(),就可以启动该线程。这种用法创造的是匿名对象,因为除了调用一次start方法外不再需要在主程序里使用对象名称调用对象里的方法或成员变量,所以对象不需要有名字,一般情况下创造一个匿名实例即可。
局限性:java只能单继承,如果使用继承thread类,那么该类就不能继承其他类
2.用Runnable接口的子类实例作为线程构造函数的参数创建线程
匿名内部类实现方法
实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免java中的单继承的局限性。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
- 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。
== 扩充==:在java中,每次程序运行至少启动2个线程。
一个是main线程,一个是垃圾收集线程。因为每当使用 java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进程。
线程的生命周期
出生: 刚用new创建出来
就绪(runnable): 其他条件都具备,只是没占用时间片,可从执行态转入,也可由其他一些状态转入。
执行(:占上时间片
死亡(Teminated):线程执行完或被结束
等待 (Waiting):调用wait(),则由执行态转为等待态。需要其他线程用notify或notifyAll唤醒至就绪态
休眠(Sleep):调用 Thread.sleep(xx),则由执行态转为休眠态,xx毫秒后会进入就绪态
阻塞 (Blocked):执行过程中某种资源或条件得不到满足,则由执行态转为阻塞态。 一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。
线程安全
以卖票为例,三个窗口同时卖票
出现了同时买一张票的情况
出现了卖出第0张,第-1张等不存在的票的情况
原因:但多线程程序中,每个线程都拥有独立的线程栈空间,在多核cpu或多cpu的电脑中,线程可以实现真正的并行,造成多个线程同时执行的现象,造成多个窗口同时买一张票,单cpu单核中实现宏观并行,微观串行:一个cpu时间片只有一个线程在执行,理论上看似线程不用处理也能安全,实则不然,在操作系统中,会出现单线程执行过程中如果遇到中断,下一个cpu时间片是另一个处理该程序的线程,也会出现线程安全问题(比如当前线程读到ticket为第1张票,还没进行卖票的时候遇到中断,下一个时间片给到另一个窗口的线程,这是之前的线程还没卖票成功,还有第0张票,于是if(ticket>0)判断为true卖票操作执行成功,
当cpu时间片切换回原来的线程时,中断的地方恰好位于if(ticket>0)内,不用判断便可以卖票,于是便卖出了第0张票(不存在),这就出现了线程安全。
目前的电脑大多都是多核cpu,这样的情况更是对于线程安全必须0容忍
线程安全的解决方案
Java中提供了同步机制 (synchronized)来解决线程安全问题。
窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码 去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU 资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象
为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制
1. 同步代码块。
synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问
synchronized(同步锁){
需要同步操作的代码
}
ps:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
- 锁对象 可以是任意类型。
- . 多个线程对象 要使用同一把锁。
- 同步锁是谁?
对于非static方法,同步锁就是this。
对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
也可以使用object类(object是所有类的父类)
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着 (BLOCKED)。
2. 同步方法
使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外 等着。
public synchronized void method(){
可能会产生线程安全问题的代码
}
3. 锁机制
java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作, 同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象
public void lock() :加同步锁。
public void unlock() :释放同步锁。
线程状态转换操作
具体常用API
1.线程的休眠
Thread.sleep进入休眠,有可能引发interruptedException(睡眠期间被其他线程interrupt),属于可检测异常,必须有 try-catch
2.线程的加入
a,b两个线程都处于启动状态,如果a线程里执行 b.join(),则a线程停止执行,直到b执行完再接着执行。
3.线程的中断
不建议在线程里使用stop方法中断,建议在run()里通过 while条件,break ,return(函数返回)等结束自身线程,也可以在线程外通过 xx.interrupt()方法。
4.线程的礼让
xx.yield();代表xx线程会把时间片礼让给同优先级其他线程
银行取钱多线程例子
public class CheckOut {
int n=0;
int money=500;
//延迟操作
public void interr() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
synchronized void toWait() {
try {
wait();
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
synchronized void toNotify() {
if(++n%3==0)
notify();
}
synchronized void input(int in,Result res) {
System.out.println(res.name+"存入"+in+" 成功 操作前金额"+money+"元"+" 操作后金额"+(int)(money+in)+"元");
money=money+in ;
res.inputm+=in;
}
synchronized void output(int out,Result res) {
if(money-out>=0) {
System.out.println(res.name+"支出"+out+" 成功 操作前金额"+money+"元"+" 操作后金额"+(int)(money-out)+"元");
money=money-out;
res.outputm+=out;
}
else
System.out.println(res.name+"支出"+out+" 失败 操作前金额"+money+"元"+" 操作后金额"+money+"元");
}
public static void main(String[] args) {
new CheckOut();
}
public CheckOut(){
Result f=new Result("父亲");
Result m=new Result("母亲");
Result s=new Result("儿子");
Thread father=new Thread(new father(f));
Thread mother=new Thread(new mother(m));
Thread son=new Thread(new son(s));
father.start();
mother.start();
son.start();
toWait();//主线程(main)进入等待状态,等以上三个线程执行完毕在执行notify()唤醒
System.out.println(f.name+"共存入"+f.inputm+" 实际取出"+f.outputm);
System.out.println(m.name+"共存入"+m.inputm+" 实际取出"+m.outputm);
System.out.println(s.name+"共存入"+s.inputm+" 实际取出"+s.outputm);
}
//父亲内部类
class father implements Runnable{
Result r;
public father(Result res) {
r=res;
}
@Override
public void run() {
// TODO 自动生成的方法存根
interr();input(1000,r);interr(); output(300,r);interr(); output(200,r);interr(); input(800,r);toNotify();
}
}
//母亲内部类
class mother implements Runnable{
Result r;
public mother(Result res) {
r=res;
}
@Override
public void run() {
// TODO 自动生成的方法存根
interr();output(100,r);interr();input(200,r);interr();output(500,r);interr();output(100,r);toNotify();
}
}
//儿子内部类
class son implements Runnable{
Result r;
public son(Result res) {
r=res;
}
@Override
public void run() {
// TODO 自动生成的方法存根
interr();output(400,r);interr();output(600,r);interr();output(100,r);interr();output(300,r);toNotify();
}
}
//创建该类的3个实例传入各自线程,用来存储各人的存取总和。
class Result{
String name;
int inputm=0;
int outputm=0;
public Result(String name){
this.name=name;
}
}
}
执行结果
线程池
Java里面线程池的顶级接口是 java.util.concurrent.Executor ,但是严格意义上讲 Executor 并不是一个线程 池,而只是一个执行线程的工具。真正的线程池接口是 java.util.concurrent.ExecutorService 。
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优 的,因此在 java.util.concurrent.Executors 线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官 方建议使用Executors工程类来创建线程池对象。
Executors类中有个创建线程池的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads) :返回线程池对象。(创建的是有界线 程池,也就是池中的线程个数可以指定最大数量)获取到了一个线程池ExecutorService 对象
public Future<?> submit(Runnable task) :获取线程池中的某一个线程对象,并执行 Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。
使用线程池中线程对象的步骤:
- 创建线程池对象。
- 创建Runnable接口子类对象。(task)
- 提交Runnable接口子类对象。(take task)
- 关闭线程池(一般不做)。
这就是基本的线程操作,接下来边学习边补充