目录
1.线程的概念
程序是静态的概念。
进程是动态的概念。
进程内又若干个线程。
2.线程的特点
- 线程就是独立执行的路径
- 在程序运行时即使没有创建线程,也会有多线程。
- 线程的运行由调度器安排调度。
3.线程的三种创建方式
Thread Class → 继承Thread类
Runnable 接口 → 实现Runnable接口
Callable 接口 → 实现Runnable接口
4.继承Thread类实现线程
步骤:
1、继承Thread类
2、重写run方法
3、调用start开启线程
public class Demo01 {
static class myThread extends Thread{
@Override
public void run() {
for (int i = 0; i< 20;i++){
System.out.println("测试线程1111111");
}
}
}
public static void main(String[] args) {
myThread thread1 = new myThread();
thread1.start();
for (int i = 0; i< 100;i++){
System.out.println("这里是主线程!!");
}
}
}
(面试可能会问)注意:
要用start方法就是开启一个线程,用新线程调用run方法。
如果直接调用run,就是创建了一个对象,对象里面调用方法没区别,不是多线程。
5.实现Runnable接口来创建线程
步骤:
- 定义一个类,实现Runnable接口
- 实现Run方法
- 创建线程对象,调用start方法启动线程
本质是个代理。
因为Java是个单继承,多接口,因此推荐使用Runnable接口来实现多线程。
public class Demo02 implements Runnable{
@Override
public void run() {
for (int i = 1;i<=20;i++){
System.out.println("测试Runnable"+i);
}
}
public static void main(String[] args) {
//1、步骤1,创建实现了Runnable接口的对象
Demo02 p = new Demo02();
//2、步骤2,创建线程对象,把start方法启动线程。
new Thread(p).start();
for(int i = 0;i<20;i++){
System.out.println("这里是主线程"+i);
}
}
}
6.lamda表达式
lamda表达式的省略过程:
1、类
2、静态内部类
3、匿名内部类
4、lamda表达式
public class Demo03 {
//方式2、静态内部类
static class Love2 implements ILove{
@Override
public void love() {
System.out.println("I Love you 2");
}
}
public static void main(String[] args) {
//1、外部类实现
ILove love = new Love1();
love.love();
//2、静态内部类实现
love = new Love2();
love.love();
//方式3、匿名内部类实现
//必须有接口或者父类才能使用匿名内部类
love = new ILove() {
@Override
public void love() {
System.out.println("I Love you 3");
}
};
love.love();
//方式4、lamda实现
love = ()->{
System.out.println("I Love you 4");
};
love.love();
}
}
interface ILove{
void love();
}
//方式1、外部类
class Love1 implements ILove{
@Override
public void love() {
System.out.println("I Love you 1");
}
}
如果把接口改成
interface ILove2{
void love(int a,String b);
}
那么,使用lamda表达式就会变成
ILove2 love = null;
//方式4、lamda实现
love = (a,b)->{
System.out.println(a+"测试多参数的lamda"+b);
};
love.love(5,"aaa");
几个总结:
- lamda表达式使用的条件是接口必须是函数式接口。(即接口下需要实现的方法只有一个)
- lamda表达式只能有一行代码的情况才能简化为一行,如果有多行,必须使用代码块包裹
- 多个参数也可以去掉参数类型,要去掉,就都要去掉,必须加上括号。
7.线程的五大状态
- 创建状态
- 就绪状态
- 阻塞状态
- 运行状态
- 死亡状态
创建状态 → 线程new的时候
就绪状态 → 当线程调用start方法的时候,立即进入就绪状态,不意味着立即执行
运行状态 → 当CPU调度就绪状态的线程,线程进入运行状态
阻塞状态 → 当调用sleep ,wait或同步锁时,线程进入阻塞状态,代码不往下执行,阻塞解除后重新进入就绪状态。
死亡状态 → 线程中断或结束,一旦进入死亡,就不能再次启动。
8.线程停止的方法
不推荐使用JDK提供的stop、destory方法
建议让线程自己停下来
建议设置一个标志位进行终止变量,当flag=false,则终止线程运行
public class Demo05 implements Runnable{
//步骤1,在外部设置标志位
private boolean flag = true;
@Override
public void run() {
int count = 0;
while(flag){
System.out.println(Thread.currentThread().getName()+"计算中"+count++);
}
}
//步骤2,在外部提供方法改变标志
public void stop(){
this.flag = false;
}
public static void main(String[] args) {
Demo05 demo05 = new Demo05();
new Thread(demo05).start();
for (int i = 0; i<=900; i++){
if(i == 800){
demo05.stop();
}
System.out.println("主线程"+i);
}
}
}
步骤1:在外部设置标志位
步骤2 :在外部提供方法改变标志
9.线程休眠
每个对象都有一把锁,sleep不会释放锁。
sleep指定当前线程阻塞的毫秒数。
Thread.sleep(1000);
10.线程礼让
正在运行的线程把CPU资源让出来,重新竞争
礼让不一定成功,有可能A把资源让出来之后,CPU还是让A继续拿资源
礼让之后,是从yield的语句继续执行下去,而不是从头开始
Thread.yield();
11.线程强制执行(插队)
当有线程使用Join的时候,相当于他插队了,要他执行完了,别的线程才能执行。
其他线程阻塞
Thread.join();
JDK对Join的描述是:
等待这个线程死亡。
12.观察线程状态
可以通过这样来获取。
Thread.State state = thread.getState();
有多少种状态?JDK文档救我。
13.线程的优先级(Priority)
线程优先级用数字来表示:
Thread.MIN_PRIORITY = 1;
Thread.MAX_PRIORITY = 10;
Thread.NROM_PRIORITY = 5;
两个方法
getPriority();
setPriority();
但是,无论优先级多高,也有可能会调用到优先级低的线程,看CPU心情
14.守护线程
线程分为用户线程和守护线程
Java虚拟机不需要等待守护线程结束(例如:垃圾回收机制)
Java虚拟机需要等待用户线程结束(例如:main函数)
当用户线程全部结束后,守护线程会随着虚拟机的结束而结束
public class Demo06 {
public static void main(String[] args) {
God god = new God();
You you = new You();
Thread thread1 = new Thread(god);
Thread thread2 = new Thread(you);
//设置守护线程
thread1.setDaemon(true);
//上帝启动
thread1.start();
//人 启动
thread2.start();
}
}
class God implements Runnable{
@Override
public void run() {
while(true){
System.out.println("上帝守护着你");
}
}
}
class You implements Runnable{
@Override
public void run() {
for(int i = 0;i<30000;i++){
System.out.println("你在活着");
}
}
}
15.线程同步(重点)
Java语言提供了一个synchronized关键字,并在Java SE 5.0 引入了ReentrantLock类。
synchronized关键字自动提供一个锁,以及相同的“条件”
使用ReentrantLock(可重入锁)的基本结构
Lock myLock = new ReentrantLock();
myLock.lock();
try{
//do something
}
finally{
myLock.unLock();
}
注意:解锁操作一定要放在finnal中,如果临界区的代码抛出异常,锁必须要被释放,不然其他线程将会永远被阻塞。
每一个对象都有自己的ReentrantLock对象,
比如Class Bank 里面定义了私有的ReentrantLock变量,并且实例化了Bank1和Bank2。
那么,如果有两个线程试图访问同一个Bank对象,那么锁将会为其提供串行服务,如下图:
如果两个线程,分别访问的是Bank1和Bank2,那他们两个线程将不会发生阻塞,因为是两个不同的对象,有两个不同的ReentrantLock。
锁是可重入的,意思是锁保持一个持有计数,被一个锁锁住的代码,可以调用同一个锁里面的方法,如:有两个在相同锁下的方法1、get和2、set,那么如果同时都使用的时候,计数是2,如果有一个已经退出了,计数是1,当计数为0时,线程释放锁。
15.1 条件对象
条件对象 = 条件变量
使用实例:
private Condition sufficientFunds;
如果发现程序不满足能够继续下去的条件:
sufficientFunds.await();
意味着:当前线程被阻塞,并且放弃了锁!
这样的话,其他线程可以使得另一线程可以进行条件的变更。
15.*重点
正在等待锁的线程 和 调用await方法的线程有本质的区别。
一旦一个线程调用了await方法,它进入该条件的等待集,当锁可用的时候,他不会马上解除阻塞。相反,他会依旧处在阻塞,直到另一个线程调用同一个条件的signAll方法为止。
因此,想要让使用了条件变量的线程继续下去,应该使用调用
sufficientFunds.signAll();
这一个调用,会重新激活因为这一个条件而等待的所有线程,他们会从等待集里面移出来,试图重新进入该对象(此时才是等待锁的线程),一旦锁可用,获得该锁,并从阻塞的地方继续执行。
这个时候,千万不能忘记要重新测试条件,因为signAll只是将线程从等待集合状态变成了等待锁状态,如果锁被其他线程拿走,该线程依旧需要等待。
通常await的调用如下:
while(! ok to process){
condition.await();
}
当一个线程拥有某个条件的锁时,它仅仅可以在该条件上调用await、signalAll、signal方法。
当一个线程调用了await()时,他没有办法重新激活自身,把希望寄托在其他线程。
如果没有其他线程来重新激活,将会导致死锁现象。
之所有很少使用signal,是因为signal是随机释放一个等待集的阻塞状态,这虽然有效,但是很危险,如果随机选择线程发现自己不能运行,将再次被阻塞,如果没有其他线程执行signal,系统死锁。
15.2 synchronized关键字
在了解了Lock和Condition对象之后,总结一下
锁和条件的关键 :
- 锁可以保护代码片段,任何时刻都只能有一个线程执行被保护的代码
- 锁可以管理试图进入被保护代码的线程
- 锁可以拥有一个或多个条件对象
- 每个条件对象管理,管理那些已经进入了被保护的代码段,但还是不能运行的代码
虽然已经有这样的机制了,但是我们其实并不需要写到这样的控制。
synchronized只用在方法上。
Java早在1.0的时候就已经给每一个对象都设置了一个内部锁,如果一个方法用synchronized关键字声明,那么对象锁将会保护整个方法。
即,下面的写法是一样的。
public synchronized void test(){
Lock myLock = new ReentrantLock();
myLock.lock();
try{
//do something
}
finally{
myLock.unLock();
}
}
public void test(){
Lock myLock = new ReentrantLock();
myLock.lock();
try{
//do something
}
finally{
myLock.unLock();
}
}
Java并且为内部锁设计了一个相关条件,即wait(),nitifyAll(),notify()。
等价于Condition里面的三个条件。
将静态方法设置为synchronized也是合法的,他会将.class对象给锁住。
Java核心技术中给出的建议:
- 最好不适用Lock/Condition,也不要使用synchronized关键字,使用java.concurrent包中的机制,会为我们处理好所有的加锁。
- 如果必须两者选其一,最好用synchronized。
15.3 Volatile域
volatile关键字为实例域提供了一种免锁机制,如果声明一个域是volatile,那么编译器和虚拟机就会认为这个域时是有可能被另一个线程并发更新的。
为了一个实体而使用一个同步方法,显然不合时宜。
15.4 典型案例:生产者消费者模式
步骤1:创建生产物chicken
步骤2:创建缓冲池类,变量count,变量chicken[10],同步方法push,同步方法pop
步骤3:创建消费者
步骤4:创建生产者
public class PandCproblem {
public static void main(String[] args) {
Cache cache = new Cache();
Customer customer = new Customer(cache);
Producer producer = new Producer(cache);
//开始消费者线程
new Thread(customer).start();
//开始生产者线程
new Thread(producer).start();
}
}
//消费者
class Customer implements Runnable{
Cache cache = null;
public Customer(Cache cache){
this.cache = cache;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//取餐100次
cache.pop();
}
}
}
//生产者
class Producer implements Runnable{
Cache cache = null;
public Producer(Cache cache){
this.cache = cache;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//生产100次
cache.push();
}
}
}
//生产物
class Chicken{
public int id;
public Chicken(int id){
this.id = id;
}
}
//缓冲池
class Cache{
private Chicken[] chickens;
private int count;
public Cache(){
//创建10个鸡的数组
chickens = new Chicken[10];
count = 0;
}
//生产
public synchronized void push(){
//判断是否缓冲池子是否已经满了,满了则等待
while(count == 10){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//装入缓冲池子
chickens[count] = new Chicken(count);
System.out.println("生产者生产第"+count+"只鸡");
count++;
//唤醒所有消费者线程
notifyAll();
}
//消费
public synchronized void pop(){
//判断缓冲池子是否为空,空则等待
while (count == 0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//取鸡,先减后放入
count--;
Chicken cur = chickens[count];
System.out.println("消费者正在取走第"+cur.id+"只鸡");
//唤醒所有生产者进程
notifyAll();
}
}