1.程序、进程、线程、并发、并行的概念
1.程序指的是代码,静态
2.进程是一个程序的执行过程
3.线程是由线程创建的,是进程的一个实体,一个进程可以拥有多个线程
举例:一个迅雷的多个下载任务,一个qq的多个聊天窗口
4.并发:宏观并行,微观串行
并行:多个同时,多核cpu可以
2.线程的基本使用
(1)java中线程的两种创建方式
1.继承Thread类,重写run方法
2.直接绕过Thread类,实现Runnable接口,重写run方法
(2)线程使用
一个类继承Thread类(或实现Runnable接口),重写run方法,写上自己的业务代码,就成为了线程。
在主函数里,调用这个对象的start方法(相当于调run里面的内容),就可以执行线程,代码如下
public class xiancheng {
public static void main(String[] args) {
Cat cat = new Cat();
cat.start();
}
}
class Cat extends Thread{
@Override
public void run() {
super.run();
System.out.println("哈喽,猫咪");
}
}
(3)线程的结构
进程执行main方法,会创造一个main线程,然后有多线程,那就在main下创建一个线程,接下来也可以在子线程创建线程,也可以在main继续创建
注意:main里面的执行结束,main线程终结,但如果子线程没有结束,那么不会退出,直到所有线程执行完,进程才退出(main终结,孩子不会终结),结构如图:
(4)线程的特点
所以线程都是并发(多核考虑并行),子线程和main线程也是并发,线程不会阻塞其他线程,如图,main和cat线程交替执行
public class xiancheng {
public static void main(String[] args) throws InterruptedException {
Cat cat = new Cat();
cat.start();
while(true) {
System.out.println("哈喽");
Thread.sleep(1000);
}
}
}
class Cat extends Thread {
@Override
public void run(){
super.run();
while(true) {
System.out.println("哈喽,猫咪");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
(5)线程的终止
如何在main中停止一个线程
1.在线程里加一个控制变量,并加上set方法:
private boolean loop = true;
public void setLoop(boolean loop) {
this.loop = loop;
}
然后在循环里加一个判断loop是否为真
在main方法里合适的时机把他设置为假就可以了
(6)线程常用方法
还有yield:线程的礼让
join:线程的插队
a.中断线程不是终止线程
一般中断的是正在休眠的线程,相当于唤醒
b.线程优先级的范围
三种常用优先级:
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
c.方法的代码举例
1.线程名字:
//在线程内部
//currentThread的意思是当前线程
Thread.currentThread().setName("线程1");
Thread.currentThread().getName(); //用于接口调用的方式,在内部
//也可直接this.setName(); //用于继承调用的方式,在内部
//在线程外部,直接用对象名字
t1.setName("线程1");
t2.getName();
2.中断:
//效果就是把正在沉睡的线程唤醒
t1.interrupt();
3.yield(礼让)
让出cpu,让其他线程执行,但礼让的时间不确定,所以也不一定礼让成功
根据操作系统判断,如果资源紧张礼让成功的概率大
4.join(插队)
插队的线程一旦插队成功,则肯定先执行完插入的线程的所有任务
使用就是,我调用t2.join,意思是让t2执行来(只有他自己串行),直到执行完在执行别的
5.用户线程和守护线程
用户线程:也叫工作线程,当线程的任务执行完或通知方式结束
守护线程:一般是为了工作线程服务的,当所有的用户线程结束,守护线程自动结束
常见的守护线程:垃圾回收机制
如果我们想主线程结束,子线程自动结束,只需要把子线程设置成守护线程即可。
t1.setDaemon(true);
(7)线程的生命周期(六状态)
1.创建态,
2.经过start后,进入就绪态和运行态
3.阻塞态(这里分为三个)分别为timewaiting(等待sleep苏醒,也叫超时等待)、wait(join等阻塞)、block(等待进入同步代码块的锁,即资源)
4.结束态
//查看状态
t1.getState();
(8)线程同步机制Synchronized(加锁)
1.概念:一些敏感数据不允许多个线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多有一个线程访问,保证数据的完整性 ,其实就是互斥。
2.具体使用:
a.同步代码块
private static final Object obj = new Object(); //唯一锁,static代表对类加锁,不加代表对对象加锁
synchronized(A.class) { //同步代码块
XXXX
}
b.synchronized还可以放在方法声明中,表示整个方法为同步方法
public synchronized static void sell(){
System.out.println("___"+ --X); //减减后输出
}
(9)线程通信
1.A线程想控制B线程,可以把B的对象传入A,调B对象里的参数进行控制
(9)问题:
a.为什么不直接调run,而是调start方法
1.因为直接调run不是启动线程,而是正常的调用方法,也就是串行
2.调用start的代码:
public synchronized void start() {
start0();
}
3.start0是本地方法,用jvm调用,底层是c/c++实现的
4.真正实现多线程的效果,是start0,不是run
实现原理:
比如对于什么时候上处理机,什么时候分配io资源,由操作系统决定
(先来先服务,时间片等等)
b.为什么有继承和接口两种线程方式
1.继承的优势:直接通过继承的方式,可以直接通过start方式创建线程,但是接口没有start这个方法,所以用起来没继承方便
2.接口的优势:第一:java是单继承,在某些情况下一个类可能已经继承了某个父类,这时再用继承的方式创建线程显然不太可能,这时候就可以通过接口,第二,接口方式更适合多个线程共享一个资源的情况,也就是说同一个对象分为两个线程执行,。
3.接口使用线程的方式:new一个thread对象,构造器参数是那个 “实现了接口的,想要成为线程的类的对象”,也就是说先把线程的类实例化,再传进Thread构造器,再通过thread.start;来创建和执行线程
Cat cat = new Cat();
Thread thread = new Thread(cat);
thread.start();
4.·接口使用线程的原理:代理设计
1.代理设计,即用一种设计方式来让一个类代理自己做某件事
2.执行过程为:实例化Thread时传入的对象,类型是Runnable,然后调用Thread这个对象的start方法,start方法会调用start0方法来创建线程,start0会执行传入的对象重写的run方法执行具体内容。
3.接下来用一段代码来模拟Thread代理设计的执行过程
class ThreadProxy implements Runnable {
private Runnable target = null;
@Override
public void run() {
if(target != null){ //第四步
target.run(); //即没被重写的run方法,是为这种代理设计模式服务的
}
}
public ThreadProxy(Runnable target) {
this.target = target; //第一步
}
public void start(){
start0(); //第二步
}
public void start0(){
run(); //第三步
}
}
上述解释未免太过啰嗦,以下一言而敝之:
把类传进去,在里面做原来有应该在外面做的事
c.同步互斥问题导致的卖票问题
问题1.会重复卖一张票
问题2.最后卖完了还会超卖
在类里定义一个商品的静态变量,然后多个线程并发卖出,最后会多卖,因为最后一轮判断的时候,没来得及修改然后都进去for循环,导致每个都减一,最后卖多了(秒杀经典问题)
还有一个重要原因:
首先要肯定的是ThreadLocal和局部变量是线程安全的,静态和实例变量都是不安全的。
内存机制中的 "副本"概念
多个线程访问一个成员变量时 每个线程都会得到一个该变量的副本 在自己的线程的栈中保存、计算 以提高速度。 但是这样就会有同步的问题了。 当一个线程修改了自己栈内副本的值 还没有立即将同步到主存中, 其他线程再来获取主存中的该变量时 就会得到过期数据。
解决办法
1.解决重复减:
synchronized(obj) { //同步代码块
System.out.println(Thread.currentThread().getName() +"___"+ --num);
}
2.解决卖多的问题:加一层判断
while (num >0){
synchronized(obj) { //同步代码块
if(num >0)
System.out.println(Thread.currentThread().getName() +"___"+ --num);
}
}
3.锁
(1)互斥锁
a.基本介绍
1.java中,引入了对象互斥锁的概念,来保证共享数据操作的完整性
2.每个对象都对应一个可称为”互斥锁“的标记,这个标记用来保证在某一时刻,只能有一个线程访问对象
3.关键字synchronized来与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问
4.同步的局限性:导致程序的执行效率降低
5.同步方法(非静态的)的锁可以是this,也可以是其他对象(要求是同一个对象)
6.同步方法(静态)的锁为当前类本身(在静态方法中,实现同步代码块,就需要传Sell.class了,不能传对象)
b. 注意事项
1.同步方法如果密钥使用static修饰,默认锁对象为this对象
2.如果方法使用了staic修饰,默认锁对象为当前类.calss
3.步骤:
分析上锁的代码
选择同步代码块或同步方法
要求多个线程的锁对象为同一个即可
(2)死锁
就和pv操作一样,这里的变量为静态或者非静态的对象
(3)释放锁
释放锁的操作:
1.同步代码块执行结束
2.当前线程在同步代码块、同步方法中遇到break、return。
3.当线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束
4.当前线程在同步代码块、同步方法中执行了线程对象的wait方法,当前线程暂停,并释放死锁。(会进Timewit阻塞,也就是暂停的意思)
不会释放锁的操作:
1.线程执行同步代码块或者同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行,但是不会释放死锁。
(案例:上厕所太困了,在坑位睡了也不会出来)
2.线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁。(该方法已过时)