多线程
首先线程是CPU的基本调度单位,
然后两种创建线程的方法(其中,方法2继承Runnable最为常用),
然后加锁两种解决线程不同步的方法。
有了锁就有可能产生死锁的问题,(线程之间都需要对方的锁标记,同时自己不会释放锁,产生死锁)
通过线程通信(wait和notify)解决死锁。
一、什么是多线程
1.1 进程:
程序是静止的,只有真正运行时的程序, 才被称为进程。
单核CPU在任何时间点上, 只能运行一个进程;
宏观并行、微观串行。//自己的理解是线程串行。
1.2 线程:
线程,又称轻量级进程(Light Weight Process)。 程序中的一个顺序控制流程,同时也是CPU的基本调度单位。
1.3 多线程:
进程由多个线程组成,彼此间完成不同的工作,交替执行,称为多线程。
例如:迅雷是一个进程,当中的多个下载任务即是多个线程。
Java虚拟机是一个进程,当中默认包含主线程(Main), 可通过代码创建多个独立线程,与Main并发执行。
1.4 进程和线程的区别:
- 进程是操作系统资源分配的基本单位,而线程是CPU的基本调度单位。
- 一个程序运行后至少有一个进程。
- 一个进程可以包含多个线程,但是至少需要有一个线程。
- 进程间不能共享数据段地址,但同进程的线程之间可以。
二、线程的组成
2.1 组成:
- 任何一个线程都具有基本的组成部分:
- CPU时间片:操作系统(OS)会为每个线程分配执行时间。
- 运行数据://堆空间共享,栈空间独立。
- 堆空间:存储线程需使用的对象,多个线程可以共享堆中的对象。
- 栈空间:存储线程需使用的局部变量,每个线程都拥有独立的栈。
- 线程的逻辑代码。
2.2 创建线程(1):
1.继承Thread类
2.覆盖run()方法
3.创建子类对象
4.调用start()方法
2.3 创建线程(2):
1.实现Runnable接口
2.覆盖run()方法
3.创建实现类对象
4 .创建线程对象
5.调用start()方法
//更为常用,也更为方便。
三、线程的状态
3.1 线程的状态(基本):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-brR3Ztsk-1600415585994)(C:\Users\Administrator\Desktop\千峰笔记\线程的状态.png)]
1.New 初始状态
线程对象被创建,即为初始状态。 只在堆中开辟内存,与常规对象无异。
2.Ready 就绪状态
调用start()之后,进入就绪状态。 等待OS选中,并分配时间片。
3.Running 运行状态
获得时间片之后,进入运行状态, 如果时间片到期,则回到就绪状态。
4.Terminated 终止状态
主线程main()或独立线程run()结束, 进入终止状态,并释放持有的时间片。
3.2 常见方法:
//都有暂停。
- 休眠://等待的是时间
- public static void sleep(long millis)
- 当前线程主动休眠 millis 毫秒。
- 放弃:
- public static void yield()
- 当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片。
- 结合://等待触发条件
- public final void join()
- 允许其他线程加入到当前线程中,并优先执行。
public class TestImplementsRunnable {
public static void main(String[] args) throws InterruptedException {
System.out.println("程序开始");
Thread.sleep(5000);
MyRunnable task = new MyRunnable();//任务Task(输出50次)
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
System.out.println("程序结束");
}
}
class MyRunnable implements Runnable{
@Override
public void run() {//任务
for (int i = 1; i <= 50; i++) {
System.out.println( Thread.currentThread().getName() + " : " + i);
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
}
}
package methods;
public class TestThreadMethods {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Task1());
Thread t2 = new Thread(new Task2());
//t1.start();
t2.start();
for (int i = 1; i <= 1000; i++) {
System.out.println("main:" + i);
if(i == 200){
t2.join();//将t2加入到我main线程中,等待t2线程的结束后,再执行。
}
}
}
}
class Task1 implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 1000; i++) {
System.out.println("Task1:" + i);
if(i % 10 == 0){
Thread.yield();//主动放弃时间片
}
}
}
}
class Task2 implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 1000; i++) {
System.out.println("---Task2:" + i);
}
}
}
3.3 线程的状态(等待):
3.4 线程状态
四、线程安全
- 需求:A线程将“Hello”存入数组的第一个空位;B线程将“World”存入数组的第一个空位。
- 线程不安全:
- 当多线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致。
- 临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性。
- 原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可打乱或缺省。
//一般来说,增删改需要加锁(线程安全),查不需要加锁(速度会快一些)。
4.1 线程同步(1):
同步代码块:
synchronized(临界资源对象){
//对临界资源对象加锁 //代码(原子操作)
}
注:
- 每个对象都有一个互斥锁标记,用来分配给线程的。
- 只有拥有对象互斥锁标记的线程,才能进入对该对象加锁的同步代码块。
- 线程退出同步代码块时,会释放相应的互斥锁标记。
线程的状态(阻塞):
public class TestSynchronized {
public static void main(String[] args) {
// 按照该卡号进行了开户,并存入了2000元
Account acc = new Account("1001", "123456", 2000D);// 0x11223344
Husband h = new Husband(acc);
Wife w = new Wife(acc);
Son s = new Son(acc);
Thread t1 = new Thread(h);
Thread t2 = new Thread(w);
Thread t3 = new Thread(s);
t1.start();// Husband
t2.start();// Wife
t3.start();// Son
}
}
class Husband implements Runnable {
Account acc;
public Husband(Account acc) {
this.acc = acc;
}
@Override
public void run() {
// synchronized(acc){//对acc对象加锁
this.acc.withdrawal("1001", "123456", 1200D);
// }
}
}
class Wife implements Runnable {
Account acc;
public Wife(Account acc) {
this.acc = acc;
}
@Override
public void run() {
// synchronized(acc){
this.acc.withdrawal("1001", "123456", 1200D);
// }
}
}
class Son implements Runnable {
Account acc;
public Son(Account acc) {
this.acc = acc;
}
@Override
public void run() {
// synchronized(acc){
this.acc.withdrawal("1001", "123456", 1200D);
// }
}
}
// 银行账户
// this = 0x11223344
class Account {
String cardNo;
String password;// w
double balance;
public Account(String cardNo, String password, double balance) {
super();
this.cardNo = cardNo;
this.password = password;
this.balance = balance;
}
// 取款
public void withdrawal(String no, String pwd, double money) {
// Wife
synchronized (this) {// this = acc (0x11223344)
System.out.println("请稍后。。。");
if (this.cardNo.equals(no) && this.password.equals(pwd)) {
System.out.println("验证成功,请稍后。。");
if (money < balance) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance -= money;
System.out.println("取款成功,当前余额为:" + balance);
} else {
System.out.println("卡内余额不足!");
}
} else {
System.out.println("卡号或密码错误!");
}
}
// Husband
}
}
4.2 线程同步(2):
同步方法:
synchronized 返回值类型 方法名称(形参列表){ //对当前对象(this)加锁
// 代码(原子操作)
}
//例如:
public synchronized void push(String str) {
}
注:
- 只有拥有对象互斥锁标记的线程,才能进入该对象加锁的同步方法中。
- 线程退出同步方法时,会释放相应的互斥锁标记。
4.3 同步规则
- 注意:
- 只有在调用包含同步代码块的方法,或者同步方法时,才需要对象的锁标记。
- 如调用不包含同步代码块的方法,或普通方法时,则不需要锁标记,可直接调用。
- 已知JDK中线程安全的类:
- StringBuffer
- Vector
- Hashtable
- 以上类中的公开方法,均为synchonized修饰的同步方法。
4.4 死锁:
当第一个线程拥有A对象锁标记,并等待B对象锁标记,同时第二个线程拥有B对象锁 标记,并等待A对象锁标记时,产生死锁。
一个线程可以同时拥有多个对象的锁标记,当线程阻塞时,不会释放已经拥有的锁标记,由此可能造成死锁。
4.5 线程通信
- 等待://高风亮节,反受其害
- public final void wait()
- public final void wait(long timeout)
- 必须在对obj加锁的同步代码块中。在一个线程中,调用obj.wait() 时,此线程会释放其拥有的所有锁标记。同时此线程因obj处在无限期等待的状态中。释放锁,进入等待队列。
- 通知:
- public final void notify()
- public final void notifyAll()
- 必须在对obj加锁的同步代码块中。从obj的Waiting中释放一个或全部线程。对自身没有任何影响。
public class TestWaitNotify {
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
MyThread t1 = new MyThread(o);
MyThread t2 = new MyThread(o);
t1.start();
t2.start();
Thread.sleep(10000);
synchronized(o){
System.out.println("main进入同步代码块");
o.notifyAll();//从那些因为o对象而进入到无限期等待的线程中,营救一个出来
System.out.println("main退出同步代码块");
}
}
}
class MyThread extends Thread{
Object obj;
public MyThread(Object obj){
this.obj = obj;
}
public void run(){
System.out.println(Thread.currentThread().getName() + "线程启动");
synchronized(obj){
System.out.println(Thread.currentThread().getName() + "进入同步代码块");
try {
obj.wait();//主动释放、无限期等待
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "退出同步代码块");
}
System.out.println(Thread.currentThread().getName() + "线程结束");
}
4.6 生产者、消费者:
//容器相关,消费者不能在空的容器中拿东西,生产者不能在满的容器中放东西。
若干个生产者在生产产品,这些产品将提供给若干个消费者去消费,为了使生产者和 消费者能并发执行,在两者之间设置一个能存储多个产品的缓冲区,生产者将生产的 产品放入缓冲区中,消费者从缓冲区中取走产品进行消费,显然生产者和消费者之间必须保持同步,即不允许消费者到一个空的缓冲区中取产品,也不允许生产者向一个满的缓冲区中放入产品。
//在offer,pool加锁解决生产者、消费者问题。
//队列
public class TestProduceAndConsumer {
public static void main(String[] args) {
MyQueue mq = new MyQueue();
Produce1 p1 = new Produce1(mq);
Produce2 p2 = new Produce2(mq);
Consumer c1 = new Consumer(mq);
p1.start();
p2.start();
c1.start();
System.out.println("main end");
}
}
class Consumer extends Thread{
MyQueue mq;
public Consumer(MyQueue mq){
this.mq = mq;
}
public void run(){
for (int i = 0 ; i < 10 ; i++) {
System.out.println(mq.poll() + "被移除");
}
}
}
class Produce1 extends Thread{
MyQueue mq;
public Produce1(MyQueue mq){
this.mq = mq;
}
public void run(){
System.out.println("Produce1启动");
for (char ch = 'A'; ch <= 'E'; ch++) {
mq.offer(ch);
}
System.out.println("Produce1结束");
}
}
class Produce2 extends Thread{
MyQueue mq;
public Produce2(MyQueue mq){
this.mq = mq;
}
public void run(){
System.out.println("Produce2启动");
for (char ch = 'F'; ch <= 'J'; ch++) {
mq.offer(ch);
}
System.out.println("Produce2结束");
}
}
//我的队列
class MyQueue {
private List values = new ArrayList();
private int max = 4;
//存入队列
public synchronized void offer(Object o){//E
while(values.size() == max){
//进来线程,停下
try {
this.wait();
//被唤醒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.notifyAll();
System.out.println(Thread.currentThread().getName() + "存入第"+ (values.size() + 1) +"个元素");
values.add(o);
}
//从队列取出
public synchronized Object poll(){//1个元素
while(values.size() == 0){
try {
this.wait();
//被唤醒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.notifyAll();//唤醒因mq对象而进入无限期等待的线程对象(一个)
return values.remove(0);
}
public void show(){
for (Object obj : values) {
System.out.println(obj);
}
}
}
测试结果:
main end
Produce1启动
Thread-0存入第1个元素
Thread-0存入第2个元素
Thread-0存入第3个元素
Thread-0存入第4个元素
Produce2启动
Thread-1存入第4个元素
A被移除
B被移除
Thread-0存入第4个元素
Produce1结束
C被移除
Thread-1存入第4个元素
D被移除
Thread-1存入第4个元素
F被移除
Thread-1存入第4个元素
E被移除
Thread-1存入第4个元素
G被移除
Produce2结束
H被移除
I被移除
J被移除
//栈
public class TestPAC {
public static void main(String[] args) {
final MyStack ms = new MyStack();//临界资源
Thread t1 = new Thread() {
public void run() {
for (char ch = 'A'; ch <= 'Z'; ch++) {
ms.push(ch+"");
}
}
};
Thread t2 = new Thread(){
public void run(){
for (int i = 0; i < 26; i++) {
ms.pop();
}
}
};
t1.start();
t2.start();
}
}
// Last In First Out
class MyStack {
private String[] values = new String[] { "", "", "", "", "" };
int size = 0;
public synchronized void push(String str) {
this.notifyAll();//先唤醒对方
while (values.length == size) {
System.out.println("满了");
try { this.wait(); } catch (Exception e) {}
}
System.out.println(str + "入栈");
values[size] = str;
size++;
}
public synchronized void pop() {
this.notifyAll();//先唤醒对方
while (size == 0) {
System.out.println("空了");
try { this.wait(); } catch (Exception e) {}
}
System.out.println(values[size - 1] + "出栈");
values[size - 1] = "";
size--;
}
}
五、总结
-
线程的创建:
- 方式1:继承Thread类
- 方式2:实现Runnable接口(一个任务Task),传入给Thread对象并执行。
-
线程安全:
- 同步代码块:为方法中的局部代码(原子操作)加锁。
- 同步方法:为方法中的所有代码(原子操作)加锁。
-
线程间的通信:
- wait() / wait(long timeout):等待
- notify() / notifyAll():通知