Java 相关的基础知识

一:Java 接口和抽象类
面对对象语言设计的重点在于抽象,Java接口(interface)和抽象类(abstract class)代表的就是抽象类型,就是我们需要提出的抽象层的具体表现。
OOP面向对象的编程,如果要提高程序的复用率,增加程序 的可维护性,可扩展性,就必须是面向接口的编程,面向抽象的编程,正确地使用接口、抽象类这些有用的抽象类型作为你结构层次上的顶层。
抽象方法
abstract void fun();
抽象方法必须用 abstract关键词修饰,一个类中含有抽象方法,则这个类为抽象类,抽象类必须在类前用abstract关键字修饰
(1)有抽象方法的类一定是抽象类,抽象类不一定有抽象方法
(2)抽象类中的抽象方法的修饰符只能为public或者protected,默认为public;
抽象类

public abstract class TestAb{

public TestAb(){
//抽象类构造方法
System.out.println("我是抽象类,我不能直接实例化,但可以被子类实例化呦");
}

 //这是抽象方法
    public abstract void fun();
    //我是非抽象方法
pub void watch(){
 System.out.println("我是非抽象方法");
}

}

//子类继承了抽象类
public class ChildTestAb extends TestAb{
public ChildTestAb (){
//抽象子类构造方法
System.out.println("我是抽象子类,我可以实例化");
}

 @Override
    public void fun() {

    }
    
    
}

//使用,可以实例化类
TestAb childTestAb=new ChildTestAb();

(1)因为抽象类中含有无具体实现的方法,所以不能用抽象类创建对象(不能直接new TestAb())
(2)一个子类继承一个抽象类,则子类必须实现父类抽象方法,否则子类也必须定义为抽象类;
(3)抽象类可以包含属性、方法、构造方法,但是构造方法不能用于实例化,主要用途是被子类调用。

接口

public interface ComputerConfigBuilder {
       int a=10;//默认:public static final int a=10;
    void setCPU();//定义接口的方法等价于public abstract void setCPU();
    void setMemery();
    void setHardDisk();
    //JDK1.8新特性,允许接口添加非抽象方法的实现,定义了default的方法可以不被实现子类所实现,但只能被实现子类的对象调用
    //如果子类实现了多个接口,并且这些接口包含一样的默认方法,则子类必须重写默认方法
    default void sayHello(){
        System.out.println("Hello World");
    }
}

(1)接口可以包含变量和方法(变量:默认变量被隐士指定为public static final,方法:默认成指定为public abstract)
(2)一个类可以实现多个接口
(3)接口不能有构造方法

二:面对对象的三大特性
继承,封装,多态
继承是所有OOP语言不可缺少的部分,在java中使用extends关键字来表示继承关系。当创建一个类时,总是在继承,如果没有明确指出要继承的类,就总是隐式地从根类Object进行继承。比如下面这段代码:

class Person{
//构造方法
public Person(){
}
}

//继承,Man继承Person
class Man extends Person{
//构造方法
public Man(){
}
}

Java 只允许单继承,但可以多层继承,一个类可以被多个类继承,也就是一个类可以拥有多个子类
注意:
变量
1.子类继承父类的成员变量,能够继承父类的public 和protected成员变量,不能够继承父类的private成员变量
2.如果子类和父类在同一个包下,则子类能够继承,否则子类不能够继承
3.对于子类可以继承的父类成员变量,如果在子类中出现了同名称的成员变量,则会发生隐藏现象,即子类的成员变量会屏蔽掉父类的同名成员变量。如果要在子类中访问父类中同名成员变量,需要使用super关键字来进行引用。
方法
4.子类继承父类的方法,能够继承父类的public和protected成员方法;不能够继承父类的private成员方法;
5.对于父类的包访问权限成员方法,如果子类和父类在同一个包下,则子类能够继承;否则,子类不能够继承;
6.对于子类可以继承的父类成员方法,如果在子类中出现了同名称的成员方法,则称为覆盖,即子类的成员方法会覆盖掉父类的同名成员方法。如果要在子类中访问父类中同名成员方法,需要使用super关键字来进行引用。
构造方法
7.子类是不能够继承父类的构造器,但是要注意的是,如果父类的构造器都是带有参数的,则必须在子类的构造器中显示地通过super关键字调用父类的构造器并配以适当的参数列表。如果父类有无参构造器,则在子类的构造器中用super关键字调用父类构造器不是必须的,如果没有使用super关键字,系统会自动调用父类的无参构造器

那么这样一看继承有一个关键词super很值得重视,那super到底是什么?
super
super 可以理解为是指向自己父类对象的一个指针,而这个父类指的是离自己最近的一个父类
super主要两种用法:
1.super.成员变量/super.成员方法
2.super(参数。。。):调用父类中某个构造方法
与之区别还有this关键字
this
this 是自身的一个对象,代表对象本身,可以理解为指向对象本身的一个指针
1.形参和成员名字重名,用this 区分

class Person {
    private int age = 10;
    public Person(){
    System.out.println("初始化年龄:"+age);
}
    public int GetAge(int age){
        this.age = age;
        return this.age;
    }
}

2.引用构造函数

//this 经常的使用方法
public class GoodsSortView extends LinearLayout{
   public GoodsSortView(Context context) {
        this(context, null);//这个是调用本类2个参数的构造方法
    }

    public GoodsSortView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);//这个是调用本类的三个参数的构造方法
    }

    public GoodsSortView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);//这个是调用父类的构造方法
    }
    
    }

封装:是把过程和数据包围起来,对数据的访问只能通过已定义的接口。在java中通过关键字private实现封装
封装靠这些访问修饰符来控制访问
private   在当前类中可访问
default(默认) 在当前包内和访问
protected 在当前类和它派生的类中可访问
public 公众的访问权限,谁都能访问

public class AccreditBean {
    private int AccountType;//这种成员变量是私有的,本类可以访问,外部访问就,用过get(),set()方法提供相当于接口来访问这个成员变量,这就是对数据的封装
    private String MemberName;
    private String NickName;

    public int getAccountType() {
        return AccountType;
    }

    public void setAccountType(int accountType) {
        AccountType = accountType;
    }

    public String getMemberName() {
        return MemberName;
    }

    public void setMemberName(String memberName) {
        MemberName = memberName;
    }

    public String getNickName() {
        return NickName;
    }

    public void setNickName(String nickName) {
        NickName = nickName;
    }

}

image.png

多态
多态指允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种的行为方式(发送消息就是函数调用)
多态:消除类型之间的耦合关系
多态存在的三个必要条件
1.要有继承
2.要有重写
3.父类引用指向子类对象
例如:List<String> list=new ArrayList<>();平时用的集合就是多态的体现

现实事物经常会体现出多种形态,如学生,学生是人的一种,则一个具体的同学张三既是学生也是人,即出现两种形态。

Java作为面向对象的语言,同样可以描述一个事物的多种形态。如Student类继承了Person类,一个Student的对象便既是Student,又是Person。
多态体现为父类引用变量可以指向子类对象。

      多态的转型分为向上转型和向下转型两种

向上转型:多态本身就是向上转型过的过程

      使用格式:父类类型 变量名=new 子类类型();

      适用场景:当不需要面对子类类型时,通过提高扩展性,或者使用父类的功能就能完成相应的操作。

向下转型:一个已经向上转型的子类对象可以使用强制类型转换的格式,将父类引用类型转为子类引用各类型

      使用格式:子类类型 变量名=(子类类型) 父类类型的变量;

例如:
父类Animal

class Animal {
int num = 10;//成员变量
static int age = 20;//静态成员变量
//成员方法
public void eat() {
        System.out.println("动物吃饭");
    }
    //静态方法
public static void sleep() {
        System.out.println("动物在睡觉");
    }

public void run(){
        System.out.println("动物在奔跑");
    }
}

子类Cat

//子类继承了父类,重写了父类的方法
class Cat extends Animal {
int num = 80;
static int age = 90;//静态变量
 String name = "tomCat";
 //重写父类的方法
 public void eat() {
        System.out.println("猫吃饭");
     }
     //子类的静态方法
     public static void sleep() {
        System.out.println("猫在睡觉");
        }
        
        //子类自己的方法
        public void catchMouse() {
        System.out.println("猫在抓老鼠");
        }
}

调用:

//向上转型:父类类型 变量名=new 子类类型();父类引用指向子类对象
Animal am = new Cat();
    am.eat();//调用子类成员方法
    am.sleep();//静态方法
    am.run();//子类没有的方法
    System.out.println(am.num);
    System.out.println(am.age);

结果:
image.png
子类Cat重写了父类Animal的非静态成员方法am.eat();的输出结果为:猫吃饭。
子类重写了父类(Animal)的静态成员方法am.sleep();的输出结果为:动物在睡觉
未被子类(Cat)重写的父类(Animal)方法am.run()输出结果为:动物在奔跑
那么我们可以根据以上情况总结出多态成员访问的特点:
成员变量
编译看左边(父类),运行看左边(父类)
成员方法
编译看左边(父类),运行看右边(子类)。动态绑定
静态方法
编译看左边(父类),运行看左边(父类)。
(静态和类相关,算不上重写,所以,访问还是左边的)
只有非静态的成员方法,编译看左边,运行看右边

多态向上转型有什么弊端了?

Animal am = new Cat();
    am.eat();//调用子类成员方法
    am.sleep();//静态方法
    am.run();//子类没有的方法
    System.out.println(am.num);
    System.out.println(am.age);
    
    //下面这是2个子类的成员方法和成员变量调用,结果是报错
//am.catchMouse();
//System.out.println(am.name);

原因就是多态的弊端,就是:不能使用子类特有的成员属性和子类特有的成员方法。

如果我们非要调用子类的成员变量和成员方法呢,
那我们就可以把这个父类引用指向了子类对象的家伙am再强制变回Cat类型。这样am就是Cat类型的引用了,指向的也是Cat对象了,自然也能使用Cat类的一切属性和一切的成员方法。

Animal am = new Cat();
    am.eat();//调用子类成员方法
    am.sleep();//静态方法
    am.run();//子类没有的方法
    System.out.println(am.num);
    System.out.println(am.age);
    
    
Cat ct = (Cat)am;
ct.eat();
ct.sleep();
ct.run();
ct.catchMouse();

image.png
执行强转语句Cat ct = (Cat)am;这个就是向下转型

三:Java内部类
在Java中,可以将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类。广泛意义上的内部类一般来说包括这四种:成员内部类、局部内部类、匿名内部类和静态内部类。
1.成员内部类

public class Circle {
    
    double radius=0;
    public Circle(double radius){
        this.radius=radius;
        
    }
    
    //成员内部类
    class Draw{
        
        public  void drawShape(){
        //可以访问外部类的成员方法和成员变量,包括静态的属性和方法
            
            System.out.println("画图");
        }
    }
}

内部类可以拥有private访问权限、protected访问权限、public访问权限及包访问权限
上面是包访问权限
调用:

 //  方式一
        //创建外部类对象
        Circle circle=new Circle(12);
        //创建内部类对象
        Circle.Draw draw=circle.new Draw();
        //调用内部类方法
        draw.drawShape();
        
        //方式二:
        new Circle(12).new Draw().drawShape();
        

如果成员内部类Inner用private修饰,则只能在外部类的内部访问,如果用public修饰,则任何地方都能访问;如果用protected修饰,则只能在同一个包下或者继承外部类的情况下访问;如果是默认访问权限,则只能在同一个包下访问

public class Circle {

    double radius=0;
    public Circle(double radius){
        this.radius=radius;
        //即只能访问,通过new Draw()对象访问方法
        new Draw().drawShape();

    }

    //成员内部类,使用private修饰符修饰的话,只能在外部类内访问
    private class Draw{

        public  void drawShape(){

            System.out.println("画图");
        }
    }
}

2.局部内部类
局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。

public class Circle {

    double radius=0;
    public Circle(double radius){
        this.radius=radius;


    }
    public  void draw(){
        //局部内部类访问修饰符,什么都不能写,private,public,protected都不能修饰
        class MyDraw{
            public void drawShape(){
                System.out.println("画图");
            }
        }
    new MyDraw().drawShape();

    }

只能方法内调用
外部调用: new Circle(12).draw();

3.匿名内部类
匿名内部类应该是平时我们编写代码时用得最多的,在编写事件监听的代码时使用匿名内部类不但方便,而且使代码更加容易维护。下面这段代码是一段Android事件监听代码:

 btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

//这里new OnClickListener

}
}

 就是匿名内部类的使用。代码中需要给按钮设置监听器对象,使用匿名内部类能够在实现父类或者接口中的方法情况下同时产生一个相应的对象,但是前提是这个父类或者接口必须先存在才能这样使用。
 匿名内部类是唯一一种没有构造器的类。正因为其没有构造器,所以匿名内部类的使用范围非常有限,大部分匿名内部类用于接口回调。
 4.静态内部类
静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或者方法,这点很好理解,因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static成员就会产生矛盾,因为外部类的非static成员必须依附于具体的对象。

public class Circle {

    double radius = 0;
    private static String aa = "我是外部类的静态成员变量";

    public Circle(double radius) {
        this.radius = radius;
        //构造方法中调用静态内部类的方法
        //在外部类中调用
        new Draw().drawShape();


    }

    private void draw() {
        //我是外部类的非静态方法
        System.out.println("我是外部类的非静态方法");
         //在外部类中调用内部类的属性和方法
        Draw.go();//可以通过静态内部类的全类名来调用静态内部类的静态方法(外部类名.静态内部类名.方法)
        Draw.d=1; //可以通过静态内部类的全类名来调用静态内部类的静态属性(外部类名.静态内部类名.属性)
        //Draw.drawShape();//不能通过类静态内部类的全类名来调用内部类的非静态属性和方法
        Draw draw=new Draw();
        draw.drawShape();
        draw.c=2;////可以通过创建内部类实例来调用静态内部类的非静态属性和方法
    }

    public static void draw2() {
        //我是外部类的静态方法
        System.out.println("我是外部类的静态方法");
    }

    //静态内部类,同样如果加权限pirvate的话,非外部类的外部就不能访问了
   public static class Draw {
         private int c;
        private static  int d;
        public Draw() {
        }
public static void go(){
    System.out.println("我是内部类的静态方法");

}

        public void drawShape() {

            System.out.println("画图");
            //System.out.println(radius);//不能直接访问外部类的非静态成员变量
            System.out.println(aa);
            //draw();不能直接访问外部类的非静态成员方法
            draw2();
            
            //但是可以通过创建外部类实例访问
              Circle circle=new Circle();
            circle.draw();   //可以通过创建外部类实例来调用外部类的非静态方法
            circle.radius=12; //可以通过创建外部类实例来调用外部类的非静态属性
        }
    }
}

调用:

//        在非外部类中:外部类名.内部类名 name = new 外部类名.内部类名();
       Circle.Draw draw= new Circle.Draw();
       draw.drawShape();

这里提到了关键词static
static关键字的作用(修饰类、方法、变量、静态块)
static修饰的类只能为内部类,普通类无法用static关键字修饰(static修饰的内部类相当于一个普通的类,访问方式为(new 外部类名.内部类的方法() )
static修饰静态方法的访问方式为 (类名.方法名: Draw.go();)
static修饰的变量为静态变量,静态变量在内存中只有一份存储空间,静态变量不属于某个实例对象,被一个类中的所有对象所共享,属于类,所以也叫类变量,可以直接通过类名来引用。
static关键字加载顺序问题:

  • 初始化父类的静态代码块...
  • 初始化子类静态代码块...
  • 10//静态变量
  • 初始化父类的构造方法...
  • 初始化子类构造方法...
  • 普通方法...

总结:
1.静态函数是可以调用类名或者对象进行调用的,而非静态函数只能使用对象进行调用。
2.静态的函数可以直接访问静态的成员,但是不能直接访问非静态的成员。

    原因:静态函数是可以使用类名直接调用的,这时候可能还没有存在对象,
    而非静态的 成员数据是随着对象 的存在而存在的。
    
  1. 非静态的函数是可以直接访问静态与非静态的成员。

     原因:非静态函数只能由对象调用,当对象存在的时候,静态数据老早就已经存在了,而非静态数据也随着对象的创建而存在了。

    4.静态函数不能出现this或者super关键字。

     原因:因为静态的函数是可以使用类名调用的,一旦使用类名调用这时候不存在对象,而this关键字是代表了一个函数 的调用者对象,这时候产生了冲突。
    

四:==和equals区别
对于基本数据类型(byte,short,char,int,float,double,long,boolean)比较的是值
对于引用数据类型,有equals和==两种方法比较
==比较的是引用,比较引用的地址值,equals方法,是object中的方法,如果不进行重写的话,比较的也是引用的地址值,实际和==一样。(但 String Integer等对equals重写了,比较的是值)
JVM把内存划分成两种:一种是栈内存,一种是堆内存。

  • 在函数中定义的一些基本类型的变量和对象的引用变量(变量名)都在函数的栈内存中分配。
  • 当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。
  • 堆内存用来存放由new创建的对象(包括由基本类型包装起来的类:Integer、String、Double,实际上每个基本类型都有他的包装类)和数组。
    image.png

五:浅析Java中的final关键字
在Java中,final关键字可以用来修饰类、方法和变量(包括成员变量和局部变量)
1.修饰类
当用final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。
2.修饰方法
如果只有在想明确禁止 该方法在子类中被覆盖的情况下才将方法设置为final的。(即该方法不能被子类重写)
3.修饰变量
 对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
 image.png

五:Java线程
 创建线程的两种方式:
 第一种继承Thread类,实现run方法
 

public class MyThread  extends Thread{

    private String name;

    public MyThread(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println(name+"下载了"+i+"%");
        }

    }
}

//调用
Thread thread1=new MyThread("线程1");
        Thread thread2=new MyThread("线程2");
        thread1.start();
        thread2.start();
        
   //结果
System.out: 线程1下载了1%
System.out: 线程2下载了1%
System.out: 线程1下载了2%
System.out: 线程2下载了2%
System.out: 线程1下载了3%
System.out: 线程1下载了4%
System.out: 线程1下载了5%
System.out: 线程2下载了3%
System.out: 线程2下载了4%
System.out: 线程2下载了5%
System.out: 线程2下载了6%
System.out: 线程2下载了7%
。。。。。

第二种方法实现Runnable接口

public class MyRun  implements Runnable{
    private String name;

    public MyRun(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println(name+"下载了"+i+"%");
        }

    }
}

//调用

        Thread thread1=new Thread(new MyRun("线程1"));
        Thread thread2=new Thread(new MyRun("线程2"));
        thread1.start();
        thread2.start();
        
        //结果
System.out: 线程1下载了1%
System.out: 线程2下载了1%
System.out: 线程1下载了2%
System.out: 线程2下载了2%
System.out: 线程1下载了3%
System.out: 线程2下载了3%

使用匿名内部类实现Runnable接口

  new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("我是匿名内部类实现的接口的线程");

            }
        }).start();
        System.out.println("aa:"+Thread.currentThread().getName());//获取当前线程名字:main
        System.out.println("bb:"+ thread1.getName());//获取thread1 
        System.out.println("cc:"+ thread2.getName());////获取threa2

线程的生命周期
image.png
1.新建:线程被new 出来;( Thread thread1=new Thread(new MyRun("线程1"));)
2.准备就绪:线程具有执行资格,即线程调用了start(),还没有执行权利( thread1.start();)
3.运行:具备执行的资格和具备执行的权利
4.阻塞:没有执行资格和执行权利
5.销毁:线程的对象变成垃圾,释放资源

sleep睡眠
Thread.sleep(long millis)和Thread.sleep(long millis, int nanos)静态方法强制当前正在执行的线程休眠(暂停执行),以“减慢线程”。当线程睡眠时,它入睡在某个地方,在苏醒之前不会返回到可运行状态。当睡眠时间到期,则返回到可运行状态。


    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(name+"下载了"+i+"%");
            try {
                Thread.sleep(1000);//线程睡眠1s
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

join,阻塞,继续执行
Thread的非静态方法join()让一个线程B“加入”到另外一个线程A的尾部。在A执行完毕之前,B不能工作

  Thread thread1=new Thread(new MyRun("线程1"));
        Thread thread2=new Thread(new MyRun("线程2"));


        thread1.start();
        try {
            thread1.join();//将“线程t1”加入到“主线程main”中,并且“主线程main()会等待它的完成”
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.start();
        //join方法是实现线程同步,可以将原本并行执行的多线程方法变成串行执行的
        
        //结果
System.out: 线程1下载了1%
System.out: 线程1下载了2%
System.out: 线程1下载了3%
。。。
System.out: 线程1下载了10%
System.out: 线程2下载了1%
System.out: 线程2下载了2%
。。。。
System.out: 线程2下载了10%

A.join:在API中的解释是:堵塞当前线程B,直到A执行完毕并死掉,再执行=>A先执行完,再执行B;

yield方法,让出位置,让其他线程先执行

    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
           
            try {
                Thread.sleep(1000);//线程睡眠1s
                Thread.yield();//让出当前线程使用权

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(name+"下载了"+i+"%");
        }

    }

A.yield:A让出位置,给B执行,B执行结束后A再执行,跟join的意思正好相反

线程的同步锁问题:
之前线程调度是根据cpu分配的时间片来去调用执行,多个线程会出现同步执行的问题
加入锁机制,实现了同步

public class MyRun  implements Runnable{
    private String name;

    public MyRun(String name) {
        this.name = name;
    }
   @Override
    public  void run() {
        synchronized (MyRun.class){//同步代码块,利用关键字synchronized 
            System.out.println("我是线程" + Thread.currentThread().getName());
            for (int i = 1; i <= 2; i++) {

                System.out.println(name+"下载了"+i+"%");
            }
            System.out.println(Thread.currentThread().getName() + "结束");
        }



    }
    }
    
    //调用
     Thread thread1=new Thread(new MyRun("线程1"));
        Thread thread2=new Thread(new MyRun("线程2"));

        thread1.start();
        thread2.start();
        //结果
System.out: 我是线程Thread-2
System.out: 线程1下载了1%
System.out: 线程1下载了2%
System.out: Thread-2结束
System.out: 我是线程Thread-3
System.out: 线程2下载了1%
System.out: 线程2下载了2%
System.out: Thread-3结束

使用特殊域变量(volatile)实现线程同步
volatile关键字为域变量的访问提供了一种免锁机制

class Bank {
            //需要同步的变量加上volatile
            private volatile int account = 100;

            public int getAccount() {
                return account;
            }
            //这里不再需要synchronized 
            public void save(int money) {
                account += money;
            }
        }

使用Lock锁来实现锁

public class MyRun  implements Runnable{
    private String name;

    private int num=10;//共享资源数据

    private Lock myLock=new ReentrantLock();//创建一个同步锁,默认是采用的非公平策略获取锁

    public MyRun(String name) {
        this.name = name;
    }

    @Override
    public  void run() {
        while (true){
            myLock.lock();
            //System.out.println("我是线程" + Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
                if (num>0){
                    System.out.println(Thread.currentThread().getName()+"余票还剩"+--num);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                myLock.unlock();
            }
        }


    }
}
//调用

        Thread thread1=new Thread(new MyRun("线程1"));
        Thread thread2=new Thread(new MyRun("线程2"));

        thread1.start();
        thread2.start();

//结果
System.out: Thread-2余票还剩9
System.out: Thread-3余票还剩9
System.out: Thread-3余票还剩8
System.out: Thread-2余票还剩8
。。。。

Synchronized与Lock的对比
相同点:

  • 协调多线程对共享对象、变量的同步访问。
  • 可重入,同一线程可以多次获得同一个锁。
  • 都保证了可见性和互斥性。
  • 都可以形成死锁。
    不同点:
  • ReentrantLock 显示获得、释放锁,synchronized 隐式获得释放锁。
  • ReentrantLock 可响应中断、可轮回,synchronized 是不可以响应中断的。
  • ReentrantLock 可以手动获得、释放锁,更加灵活,synchronized 自动释放锁,更方便、安全。
  • ReentrantLock 是API级别的,synchronized是JVM级别的。
  • ReentrantLock 可以实现公平锁和非公平锁, synchronized只能实现非公平锁。
  • ReentrantLock 可以拿到锁的状态,所以可以判断是否成功获得锁,synchronized无法获得锁状态。
  • ReentrantLock 通过Condition可以绑定多个条件,可以轻松的解决生产者消费者问题 。
  • ReentrantLock 发生异常时如果没用主动释放锁可能会导致死锁,synchronized 发生异常会自动释放锁。
  • ReentrantLock 底层通过AQS实现,synchronized 底层通过 monitorenter 和 monitorexit 这个两个字节码指令实现。
  • ReentrantLock 可以通过tryLock 方法实现限时等待获取锁。

wait()、notify/notifyAll() 方法
1.wait()、notify/notifyAll() 方法是Object的本地final方法,无法被重写。
2.wait()使当前线程阻塞,前提是 必须先获得锁,一般配合synchronized 关键字使用,即,一般在synchronized 同步代码块里使用 wait()、notify/notifyAll() 方法。
3.notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法
4.由于 wait()、notify/notifyAll() 在synchronized 代码块执行,说明当前线程一定是获取了锁的。
当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。
只有当 notify/notifyAll() 被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次释放锁。

 
 END: 回避现实的人,未来将更不理想


Rocky_ruan
57 声望5 粉丝

不积跬步,无以至千里