路是自己选的
- 多线程
- 概念
- 优缺点
- 多线程创建
- 线程的五种状态
- 阻塞状态
- 线程基本信息
- 线程同步
- 线程死锁
- 生产者和消费者模式
一、概念
1、程序
Java源程序和字节码文件被称为"程序",是一个静态的概念。
2、进程
执行中的程序叫做进程(Process),是一个动态的概念。
-
进程是程序的一次动态执行过程, 占用特定的地址空间.
-
每个进程由3部分组成:cpu,data,code。每个进程都是独立的,保有自己的cpu时间,代码和数据,即便用同一份程序产生好几个进程,它们之间还是拥有自己的这3样东西。
-
多任务(Multitasking)操作系统将CPU时间动态地划分给每个进程,操作系统同时执行多个进程,每个进程独立运行。以进程的观点来看,它会以为自己独占Cpu的使用权
进程的查看
- Windows系统: Ctrl+Alt+Del
- Unix系统: ps or top
3、线程
- 线程是进程中一个“单一的连续控制流程” (a single sequential flflow of control)执行路径。线程也可以达到同一份程序产生好几个进程的效果,但是不同的线程之间可以有某种程度上的资源共享,所以线程又被称为轻量级进程(lightweight process)。
- Threads run at the same time, independently of one another
- 一个进程可以拥有多个并行的线程
- 一个进程中的线程共享相同的内存单元/内存地址空间可以访问相同的变量和对象,而且它们从同一堆中分配对象通信、数据交换、同步操作
- 由于线程间的通信是在同一地址空间上进行的,所以不需要额外的通信机制,这就使得通信更简便而且信息传递的速度也更快。
程序是指令的集合,代码的集合;而进程是动态的概念,当程序在执行时,系统分配进程;多线程是在同一进程下,充分利用资源 ,多条执行路径,共享资源 (cpu data code)。
注意:很多线程是模拟出来的,真正的多线程是指有多个 cpu,即多核,如服务器。如 果是模拟出来的多线程,即一个 cpu 的情况下,在同一个时间点,cpu 只能执行一个代码, 因为切换的很快,所以就有同时执行的错觉。但是现在的电脑一般都是多核的。
二、优缺点
1、优点
资源利用率更好;程序设计在某些情况下更简单;程序响应更快
2、缺点
设计复杂
三、多线程的创建和启动
1、Thread子类
1.1、创建
public class MyThread extends Thread{
@Override // 重写父类方法
public void run() {
System.out.println("MyThread...");
}
}
1.2、启动
public class Test1 {
public static void main(String[] args) {
Thread thread = new MyThread();
thread.start(); // 开启线程
}
}
2、Runnable接口
2.1、创建
public class MyThread implements Runnable{
@Override // 重写父类方法
public void run() {
System.out.println("Runnable...");
}
}
2.2、启动
public class Test1 {
public static void main(String[] args) {
Thread thread = new Thread(new MyThread());
thread.start();
}
}
3、匿名内部类
public class Test1 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程");
}
}).start();
}
}
4、Lambda表达式
public class Test1 {
public static void main(String[] args) {
new Thread(()->System.out.println("线程")) .start();
}
}
四、线程的五种状态
- 新生状态
- 就绪状态
- 运行状态
- 阻塞状态
- 死亡状态
五、阻塞状态
1、sleep
当一个线程执行代码的时候调用了sleep方法后,线程处于睡眠状态,需要设置一个睡眠时间,此时有其他线程需要执行时就会造成线程阻塞,而且sleep方法被调用之后,线程不会释放锁对象,也就是说锁还在该线程手里,CPU执行权还在自己手里,等睡眠时间一过,该线程就会进入就绪状态,典型的“占着茅坑不拉屎”;
public class Test2 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0;i<10;i++) {
if(i%2==0) {
try {
Thread.sleep(500); // 阻塞500毫秒,500毫秒走完就进入就绪状态
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("A:"+i);
}
}
}).start();
}
}
2、yield
当一个线程正在运行时,调用了yield方法之后,该线程会将执行权礼让给同等级的线程或者比它高一级的线程优先执行,此时该线程有可能只执行了一部分而此时把执行权礼让给了其他线程,这个时候线程会进入就绪状态,等待系统调度,这就很”中国化的线程“了,比较讲究谦让;
public class Test2 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0;i<100;i++) {
if(i%2==0) {
Thread.yield(); // 礼让,让出cpu的使用权,直接进入就绪状态,等待系统的调度
}
System.out.println("A:"+i);
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0;i<100;i++) {
System.out.println("B:"+i);
}
}
}).start();
}
}
3、join
当一个线程正在运行时,调用了一个join方法,此时在哪个线程里调用join方法,哪个线程就会进入阻塞状态,等待调用join方法的线程执行完毕,才会再次执行自己。就好比插队,本来该执行自己的,但是别的找关系,插了个队,只能把自己的阻塞,等待插队的走完,才能再次走自己的。
public class Test2 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0;i<10;i++) {
System.out.println("AAAA:"+i);
}
}
});
t1.start();
for(int i = 0;i<10;i++) {
if(i==2) { // 在i==2的时候,t1插个队,阻塞main线程,执行t1
t1.join();
}
System.out.println("Main:"+i);
}
}
}
六、线程基本信息
public class Test3 {
public static void main(String[] args) {
System.out.println(Thread.currentThread()); // 获取当前线程
System.out.println(Thread.currentThread().isAlive()); // 线程是否还或者(未死亡)
System.out.println(Thread.currentThread().getPriority()); // 获取线程优先级,默认线程优先级为5
Thread.currentThread().setPriority(10); // 设置线程优先级[0,10]
System.out.println(Thread.currentThread().getName()); // 获取线程名字
Thread.currentThread().setName("Ahh"); // 设置线程名字
}
}
七、线程同步
由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题。Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问。
public class Ticket {
public int count;
public Ticket(int count) {
this.count = count;
}
}
public class TicketConsumer implements Runnable{
private Ticket ticket;
public TicketConsumer(int count) {
super();
this.ticket = new Ticket(count);
}
@Override
public void run() {
while(true) {
try {
Thread.sleep(100); // 模拟抢票延迟
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if((ticket.count)>0) {
System.out.println(Thread.currentThread().getName()+"抢到第"+ticket.count--+"张票");
}else {
break;
}
}
}
}
public class Test4 {
public static void main(String[] args) {
TicketConsumer ticket = new TicketConsumer(10); // 共10张票
Thread t1 = new Thread(ticket,"张三");
Thread t2 = new Thread(ticket,"李四");
t1.start();
t2.start();
}
}
运行结果 |
---|
张三抢到第10张票 李四抢到第9张票 张三抢到第8张票 李四抢到第7张票 张三抢到第6张票 李四抢到第5张票 张三抢到第4张票 李四抢到第4张票 李四抢到第3张票 张三抢到第2张票 李四抢到第1张票 张三抢到第1张票 |
可以看到结果不对,共10张票,但是出现了2次重复车票,这就是因为不同步问题,就上面例子来说,李四抢到了第4张车票,但是可能就在出票的的时候,系统分配给李四的时间片用完了,这时李四就会进入阻塞状态,这时候张三也来抢票,抢到了第四张,张三抢完了,系统分配给张三的时间片也用完了,张三进入阻塞状态,然后解除李四的阻塞继续执行,但是李四并不知道,第四张票已经被张三抢了,仍旧会抢第四张车票,这就是为什么重复抢到了车票。
Java中有一个关键字可以解决这种问题,那就是synchronized
1、synchronized同步
public class TicketConsumer implements Runnable{
private Ticket ticket;
public TicketConsumer(int count) {
super();
this.ticket = new Ticket(count);
}
@Override
public void run() {
while(true) {
try {
Thread.sleep(100); // 模拟抢票延迟
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (ticket) {
if((ticket.count)>0) {
System.out.println(Thread.currentThread().getName()+"抢到第"+ticket.count--+"张票");
}else {
break;
}
}
}
}
}
运行结果 |
---|
张三抢到第10张票 李四抢到第9张票 张三抢到第8张票 李四抢到第7张票 张三抢到第6张票 李四抢到第5张票 李四抢到第4张票 张三抢到第3张票 张三抢到第2张票 李四抢到第1张票 |
八、线程死锁
死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
public class Test5 {
static String a = "A";
static String b = "B";
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (a) {
System.out.println(Thread.currentThread().getName()+"拿到了a,准备拿b");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (b) {
System.out.println();
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (b) {
System.out.println(Thread.currentThread().getName()+"拿到了b,准备拿a");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (a) {
System.out.println();
}
}
}
});
t1.start();
t2.start();
}
}
运行结果 |
---|
Thread-0拿到了a,准备拿b Thread-1拿到了b,准备拿a |
Tips:上述程序一直没有结束
上述就是一个死锁,t1
想先拿a,然后拿b
,t2
想先拿b
然后拿a
t1
和t2
两个线程,如果t1
先执行,t1
拿到了a
,然后t1
就会sleep(100)
毫秒进入阻塞状态,t2
就会执行,t2
拿到了b
,然后t2
进入阻塞状态,t1
解除阻塞状态继续拿b
,但是因为b
已经被t2
拿了,t1
就会等待t2
释放b
,然后才能继续执行,但是同样t2
也在等待t1
释放a
,两者就会僵持下去,这就形成了死锁
解决死锁:
- 死锁往往是程序逻辑问题,修改程序逻辑
- 尽量不要同时持有两个对象锁
九、生产者和消费者模式
在常见的多线程问题解决中,同步问题的典型示例是“生产者-消费者”模型,也就是生产者线程只负责生产,消费者线程只负责消费,在消费者发现无内容可消费时则睡觉
public class Factory {
List<Integer> list = new ArrayList<Integer>();
public synchronized void a() {
if(list.size()!=10) { // 不满足则一直生产
list.add(1);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("生产:"+list.size());
}else { // 满足执行
this.notify(); // 叫醒另外一个线程干活
try {
this.wait(); // 睡觉
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public synchronized void b() {
if(list.size()!=0) { // 一直消费
list.remove(list.size()-1);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("消费:"+list.size());;
}else { // 消费完毕
this.notify(); // 叫醒另外一个线程干活
try {
this.wait(); // 睡觉
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public class Test6 {
static List<Integer> list = new ArrayList<Integer>();
public static void main(String[] args) {
Factory f = new Factory();
// 生产者
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while(true) {
f.a();
}
}
});
// 消费者
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while(true) {
f.b();
}
}
});
t1.start();
t2.start();
}
}