------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------
一、多线程
1,了解多线程之前需要明确一些概念:
进程:正在进行中的程序(直译)
线程:就是进程中一个负责程序执行的控制单元(执行路径)
一个进程中可以有多个执行路径,称之为多线程。一个进程中至少有一个线程。开启多个线程是
为了同时运行多部分代码,每一个线程都有自己运行的内容,这个内容可以称之为线程要执行的任务。
多线程技术的目的是为了解决多个程序同时运行的问题。但是线程太多会使得效率降低。
程序的执行就是CPU在做着快速切换完成的,这个切换是随机的。
JVM在启动时启动了多个线程,至少有两个线程可以分析出来。
a,执行main函数的线程。该线程的任务代码都定义在main函数中
b,负责垃圾回收的线程。该线程的任务代码定义在垃圾回收器中(用到finalize方法)。,
自定义线程的任务在哪里呢?Thread类用来描述线程,线程是需要任务的,所以Thread类也对任务进行
描述,这个任务就是通过Thread类中的run方法来体现,run方法就是封装自定义线程任务的方法,run方法
中定义的就是要运行任务的代码。
2,在Java中,Thread类用来描述多线程,如何创建一个线程呢?
方式一:定义一个类继承Thread类, 覆盖Thread类中的run 方法,创建Thread子类对象,使用start方法开启线程。
演示代码:
public class ThreadDemo {
public static void main(String[] args) {
// 创建子类对象并调用start方法,开启多个线程
new Task("wangcai").start();
new Task("小强").start();
for(int x=0; x<5; x++){
// Thread。currentThread。getName可以获得当前运行的线程对象
System.out.println(x+"......"+Thread.currentThread().getName());
}
// 这里会出现异常ArithmeticException,但是并不影响其他线程的运行
System.out.println(4/0);
}
}
/*
* 定义一个类继承Thread类
*/
class Task extends Thread{
private String name;
public Task(String name){
// 如果使用父类构造函数super(name),线程名称就是构造函数里的name
super(name);
this.name = name;
}
// 覆盖run方法
public void run(){
for(int x=0; x<5; x++){
// getName方法可以获得线程的名称
System.out.println(name+"......"+x+"......"+getName());
}
}
}
上述代码中的getName和Thread.currentThread.getName()方法可以获得线程的名称。
方式二:定义一个类实现Runnable接口,覆盖接口中的run方法,将线程任务代码封装到run方法中,
通过Thread类创建线程对象,将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递,
调用线程对象的start方法开启线程。方式二更为常用。
演示代码:
public class ThreadDemo2 {
public static void main(String[] args) {
// 创建Runnable接口子类对象
Task2 t = new Task2();
// 创建线程对象,并将Runnable接口子类对象最为构造函数的参数进行传递
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
// 调用start方法,开启线程
t1.start();
t2.start();
}
}
// 定义一个类实现Runnable接口
class Task2 implements Runnable{
// 覆盖run方法,并将线程任务封装到run方法中
public void run(){
show();
}
private void show() {
for(int x=0; x<5; x++){
System.out.println(Thread.currentThread().getName()+"......"+x);
}
}
}
a,将线程任务从线程子类对象中分离出来,进行单独的封装,按照面向对象的思想将任务封装成对象
b,避免了java单继承的局限性
3,线程中的一些概念。
start():运行该方法,开启线程。
sleep(time):运行该方法,线程会被冻结指定长度的时间。时间结束,线程就会恢复
wati():运行该方法,线程就会被冻结,直到调用notify()方法才会恢复。
CPU的执行资格:可以被CPU处理,处理队列中排队等待
CPU的执行权:正在被CPU处理
线程处于运行状态:具备执行资格,具备执行权
线程处于冻结状态:释放执行权,释放执行资格
线程临时阻塞状态:具备执行资格,不具备执行权,正在等待执行权
4,卖票示例:
同一个线程只能开启一次,多次开启会抛出IllegalThreadStateException
当多个线程操作一个共同的对象时,会引发线程安全问题。产生的原因如下:
前提:a,多个线程在同时操作共享的数据。b,共享数据的任务代码有多条
原因:当一个线程在执行操作共享数据的多条任务代码的过程中,其他线程参与了运算,
就会导致线程安全问题的产生。
演示代码:
public class ThreadDemo3 {
public static void main(String[] args) {
// 创建接口子类对象
Ticket t = new Ticket();
// 将接口子类对象作为线程对象构造函数的参数进行传递,并调用start方法
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
// 定义一个类实现Runnable接口
class Ticket implements Runnable{
private int num = 100;
@Override
// 覆盖run方法,将要执行的线程任务代码封装到run方法中
public void run() {
// TODO Auto-generated method stub
while(true){
if(num>0){
// 为了展示线程安全问题,使用了sleep方法,异常无法再run方法上声明,只能用try/catch语句
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
System.out.println("Ticket--:"+Thread.currentThread().getName()+"---"+num--);
}
}
}
}
5,由卖票示例发现了线程安全问题,即多个线程同时操作共享数据(数据代码有多条)时,会出现
线程安全的问题,那么该如何解决呢?
解决思路:将共享数据的多条任务代码封装起来,当某一线程在执行这些代码的时候,其他线程不可以
参与运算,必须等待该线程将这些代码执行完后,其他线程才可以参与运算。
在java中,用同步代码块可以解决该问题,同步代码块的格式如下:
synchronized(对象){
要被同步的代码;
}
同步代码块的好处:解决了线程安全的问题
同步代码块的弊端:相对降低了效率,因为同步外的线程都会判断同步锁
同步代码块的前提:必须有多个线程使用同一个锁
演示代码:
public class ThreadDemo3 {
public static void main(String[] args) {
// 创建接口子类对象
Ticket t = new Ticket();
// 将接口子类对象作为线程对象构造函数的参数进行传递,并调用start方法
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
// 定义一个类实现Runnable接口
class Ticket implements Runnable{
private int num = 100;
// 创建一个Objcet的对象,用于同步代码块的锁
Object obj = new Object();
@Override
// 覆盖run方法,将要执行的线程任务代码封装到run方法中
public void run() {
// 如果锁的位置放在run方法中,安全问题依然存在,因为不同的线程使用的是不同的锁
// Object obj = new Object();
while(true){
synchronized(obj){
if(num>0){
// 为了展示线程安全问题,使用了sleep方法,异常无法再run方法上声明,只能用try/catch语句
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
System.out.println("Ticket--:"+Thread.currentThread().getName()+"---"+num--);
}
}
}
}
}
6,银行存钱示例
public class ThreadDemo4 {
public static void main(String[] args) throws InterruptedException {
// 创建Runnable接口的子类对象
Cus cus = new Cus();
// 将Runnable接口子类对象作为Thread对象的构造函数的参数进行传递,并开启线程
new Thread(cus).start();
new Thread(cus).start();
}
}
class Bank{
private int sum;
// 创建同步锁对象
private Object obj = new Object();
// 函数上也可以加同步,称为同步函数
public /*synchronized*/ void add(int num){
synchronized(obj){ // 添加同步锁
for(int x=0; x<3; x++){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
sum+=num;
System.out.println("Sum:"+sum);
}
}
}
}
class Cus implements Runnable{
Bank b = new Bank();
@Override
public void run() {
b.add(100);
}
}
同步函数和同步代码块同时应用时,应该用同一个锁,如果不是同一个锁,仍然会存在线程安全问题。
同步函数与同步代码块的区别:
同步函数的锁是固定的this;同步代码块的锁可以是任意对象,在实际开发中,建议使用同步代码块
静态同步函数使用的锁是:该函数所属字节码文件对象,可以用getClass方法获取,也可以用当前的
类名.class表示。
总之:不论是用同步代码块还是同步函数/静态同步函数,只需给他们用同一个锁即可。
此外注意:加同步锁的位置需要特别注意!
演示代码:
public class ThreadDemo5 {
public static void main(String[] args) throws InterruptedException {
Ticket2 t = new Ticket2();
new Thread(t).start();
Thread.sleep(10);
t.flag = false;
new Thread(t).start();
}
}
class Ticket2 implements Runnable{
private int num = 100;
public boolean flag = true;
Object obj = new Object(); // 创建一个Objcet的对象,用于同步代码块的锁
/*
* 如果同步锁加载这个位置,一个线程将执行完所有的任务,
* 其他线程永远不会进去,因此要把同步锁加到合适的位置
*/
public /*synchronized*/ void run() {
if(flag)
while(true){
// 同步代码块
// 如果同步代码块用obj锁,那么与同步函数的锁就不同了,依旧存在线程安全问题
synchronized(/*obj*/this){
if(num>0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
System.out.println("obj :"+Thread.currentThread().getName()+"---"+num--);
}
}
}
else
while(true)
sale();
}
//在函数上添加同步,同步函数,同步锁是this
// 如果函数是静态的。那么函数的锁不是this,而是函数所属字节码文件对象
private synchronized void sale() {
if(num>0){
// 为了展示线程安全问题,使用了sleep方法,异常无法再run方法上声明,只能用try/catch语句
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
System.out.println("function:"+Thread.currentThread().getName()+"---"+num--);
}
}
}
7,单例模式中饿汉式的线程安全问题
class Single{
private static Single s = null;
private Single2(){
}
public static Single getSingle(){
if(s==null){ // 提高了效率,对象被建立后就不需要再判断锁了
synchronized(Single.class){ // 解决了线程安全问题
if(s==null)
s = new Single();
}
}
return s;
}
}
8,死锁问题:常见场景之一,同步的嵌套。
public class DeadLockDemo {
public static void main(String[] args) throws InterruptedException {
DeadLock d = new DeadLock();
new Thread(d).start();
Thread.sleep(10);
d.flag = false;
new Thread(d).start();
}
}
class DeadLock implements Runnable{
public boolean flag = true;
// 封装线程任务
public void run(){
if(flag){
while(true){
// 一个线程拿到locka锁后,区拿lockb锁时,可能另一线程拿到了lockb锁,
// 准备拿locka锁,此时就会产生死锁现象、
synchronized(MyLock.locka){
System.out.println("true locka is run......");
synchronized(MyLock.lockb){
System.out.println("true lockb is run......");
}
}
}
}else{
while(true){
synchronized(MyLock.lockb){
System.out.println("false locka is run......");
synchronized(MyLock.locka){
System.out.println("false lockb is run......");
}
}
}
}
}
}
// 创建两个对象作为线程的锁
class MyLock{
public static final Object locka = new Object();
public static final Object lockb = new Object();
}
9,线程间通信
多个线程在处理同一资源,但是任务不同。
等待/唤醒机制设计的方法:
a,wait():让线程处于冻结状态,被wait的线程会存储到线程池中
b,notify():唤醒线程池中的一个线程(任意的)
c,notifyAll():唤醒线程池中所有的线程
这些方法必须定义在同步中,因为这些方法时用于操作线程状态的方法,
必须要明确到底操作的是哪个锁上的线程。
这些方法都定义在Object类中,为什么呢?因为这些 方法是监视器的方法,监视器其实就是锁,锁可以
是任意的对象,任意的对象调用的方法一定定义在Object中。
两个线程操作同一资源时,首先要注意线程安全问题,即多个线程用到的锁是同一个锁。
如果想让两个线程轮流操作同一资源时,就要用到等待/唤醒机制。
演示代码:
/*
* 对一个人根据条件进行命名,然后输出这个人的属性
*/
public class ThreadDemo8 {
public static void main(String[] args) {
// 创建资源对象
Resource2 r = new Resource2();
// 将对象作为线程对象构成函数的参数进行传递,并开启线程
new Thread(new Input2(r)).start();
new Thread(new Output2(r)).start();
}
}
class Resource2{
// 初始化变量
private String name;
private String sex;
private boolean flag = false;
// 同步函数,用到的锁是this
public synchronized void set(String name, String sex){
// 对资源的状态进行判断,从而决定是否等待或唤醒
if(flag)
try {
this.wait(); // 等待或唤醒,需要由监视器(锁对象)来执行
} catch (InterruptedException e) {
}
this.name = name;
this.sex = sex;
this.flag = true;
this.notify(); // 等待或唤醒,需要由监视器(锁对象)来执行
}
// 同步函数,用到的锁是this
public synchronized void out(){
// 对资源的状态进行判断,从而决定是否等待或唤醒
if(!flag)
try {
this.wait(); // 等待或唤醒,需要由监视器(锁对象)来执行
} catch (InterruptedException e) {
}
System.out.println(this.name+"---"+this.sex);
this.flag = false;
this.notify(); // 等待或唤醒,需要由监视器(锁对象)来执行
}
}
class Input2 implements Runnable{
private Resource2 r;
// 将资源类型变量传入构造函数,确保输入输出用的是同一对象
Input2(Resource2 r){
this.r = r;
}
public void run(){
int x=0;
while(true){
if(x==0)
r.set("mike", "nan");
else
r.set("丽丽", "女女女女");
x = (x+1)%2;
}
}
}
class Output2 implements Runnable{
private Resource2 r;
// 将资源类型变量传入构造函数,确保输入输出用的是同一对象
Output2(Resource2 r){
this.r = r;
}
public void run(){
while(true)
r.out();
}
}
10,等待/唤醒机制的经典示例------生产者消费者
多生产多消费,比如有3个人生产烤鸭,3个人消费烤鸭。如果烤鸭数量为0,那么3个人中随机一人生产烤鸭。
如果烤鸭数量不为0,那么就不在生产,等待消费后数量变为0再生产。
烤鸭就是公共资源,3个人生产则是3个生产线程,3个消费则是3个消费线程。
流程应该是这样:3个生产线程随机一个生产了一只烤鸭,3个消费线程随机消费了一只烤鸭,3个生产线程再随机
生产一只烤鸭......,交替循环。
演示代码:
public class ThreadDemo9 {
public static void main(String[] args) {
// 创建公共资源的对象
Duck d = new Duck();
// 将该对象分别封装到生产和消费线程,再将消费或生产线程的对象作为Thread对象的构造函数
// 的参数进行传递,并开启线程
new Thread(new Producer(d)).start();
new Thread(new Consumer(d)).start();
new Thread(new Producer(d)).start();
new Thread(new Consumer(d)).start();
}
}
class Duck{
private String name;
private int count = 1;
private boolean flag = false;
public synchronized void setDuck(String name){
// 如果while改成if的话,会出现不判断标记,而直接执行标记后面的语句
// 使用while确保线程每次都会判断标记
while(flag)
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
this.name = name+count;
System.out.println(this.name+"++++++++");
count++;
this.flag = true;
// 使用notify,一次只唤醒一个线程,无法确保唤醒对方线程,如果唤醒本方线程,无意义
// while+notify会导致死锁现象
// 使用notifyAll唤醒所有线程池中的线程,确保对方线程能够唤醒
this.notifyAll();
}
public synchronized void getDuck(){
while(!flag)
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.name+"--------");
this.flag = false;
this.notifyAll();
}
}
// 将任务进行封装
class Producer implements Runnable{
// 将Duck进行封装,确保公共资源的唯一性
private Duck duck;
public Producer(Duck duck){
this.duck = duck;
}
public void run(){
while(true){
duck.setDuck("烤鸭");
}
}
}
//将任务进行封装
class Consumer implements Runnable{
// 将Duck进行封装,确保公共资源的唯一性
private Duck duck;
public Consumer(Duck duck){
this.duck = duck;
}
public void run(){
while(true){
duck.getDuck();
}
}
}
The End