分类目录归档:JAVA

Java生产者消费者模型实践二

BlockingQueue的写法最简单。核心思想是,把并发和容量控制封装在缓冲区中。而BlockingQueue的性质天生满足这个要求。

package com.github.xuchengen.concurrent.impl;

import com.github.xuchengen.concurrent.AbsConsumer;
import com.github.xuchengen.concurrent.AbsProducer;
import com.github.xuchengen.concurrent.Model;
import com.github.xuchengen.concurrent.Task;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 基于阻塞队列的缓冲实现
 * 作者:徐承恩
 * 邮箱:xuchengen@gmail.com
 * 日期:2019/12/11
 */
public class BlockingQueueModel implements Model {

    private final BlockingQueue<Task> queue;

    private final AtomicInteger increTaskNo = new AtomicInteger();

    public BlockingQueueModel(int cap) {
        this.queue = new LinkedBlockingQueue<>(cap);
    }

    @Override
    public Runnable newRunnableConsumer() {
        return new ConsumerImpl();
    }

    @Override
    public Runnable newRunnableProducer() {
        return new ProducerImpl();
    }

    private class ConsumerImpl extends AbsConsumer {

        @Override
        public void consume() throws InterruptedException {
            Task task = queue.take();
            // 固定时间范围的消费,模拟相对稳定的服务器处理过程
            Thread.sleep(500 + (long) (Math.random() * 500));
            System.out.println("consume: " + task.no);
        }
    }

    private class ProducerImpl extends AbsProducer {

        @Override
        public void produce() throws InterruptedException {
            // 不定期生产,模拟随机的用户请求
            Thread.sleep((long) (Math.random() * 1000));
            Task task = new Task(increTaskNo.getAndIncrement());
            System.out.println("produce: " + task.no);
            queue.put(task);
        }
    }

    public static void main(String[] args) {

        Model model = new BlockingQueueModel(3);

        //2个消费者
        for (int i = 0; i < 2; i++) {
            new Thread(model.newRunnableConsumer()).start();
        }

        //5个生产者
        for (int i = 0; i < 5; i++) {
            new Thread(model.newRunnableProducer()).start();
        }
    }

}

由于操作“出队/入队+日志输出”不是原子的,所以上述日志的绝对顺序与实际的出队/入队顺序有出入,但对于同一个任务号task.no,其consume日志一定出现在其produce日志之后,即:同一任务的消费行为一定发生在生产行为之后。缓冲区的容量留给读者验证。符合两个验证条件。

BlockingQueue写法的核心只有两行代码,并发和容量控制都封装在了BlockingQueue中,正确性由BlockingQueue保证。面试中首选该写法,自然美观简单。

Java生产者消费者模型实践一

考查Java的并发编程时,手写“生产者-消费者模型”是一个经典问题。有如下几个考点:

  • 对Java并发模型的理解
  • 对Java并发编程接口的熟练程度
  • bug free
  • coding style

JDK版本:oracle java 1.8.0_102

本文主要归纳了4种写法,阅读后,最好在白板上练习几遍,检查自己是否掌握。这4种写法或者编程接口不同,或者并发粒度不同,但本质是相同的——都是在使用或实现BlockingQueue。

生产者-消费者模型

网上有很多生产者-消费者模型的定义和实现。本文研究最常用的有界生产者-消费者模型,简单概括如下:

  • 生产者持续生产,直到缓冲区满,阻塞;缓冲区不满后,继续生产
  • 消费者持续消费,直到缓冲区空,阻塞;缓冲区不空后,继续消费
  • 生产者可以有多个,消费者也可以有多个

可通过如下条件验证模型实现的正确性:

  • 同一产品的消费行为一定发生在生产行为之后
  • 任意时刻,缓冲区大小不小于0,不大于限制容量

该模型的应用和变种非常多,不赘述。

准备

面试时可语言说明以下准备代码。关键部分需要实现,如AbsConsumer。

下面会涉及多种生产者-消费者模型的实现,可以先抽象出关键的接口,并实现一些抽象类:

package com.github.xuchengen.concurrent;

/**
 * 生产者接口
 * 作者:徐承恩
 * 邮箱:xuchengen@gmail.com
 * 日期:2019/12/11
 */
public interface Producer {

    /**
     * 生产者负责生产
     *
     * @throws InterruptedException 线程意外终止异常
     */
    void produce() throws InterruptedException;

}
package com.github.xuchengen.concurrent;

/**
 * 消费者接口
 * 作者:徐承恩
 * 邮箱:xuchengen@gmail.com
 * 日期:2019/12/11
 */
public interface Consumer {

    /**
     * 消费者负责消费
     *
     * @throws InterruptedException 线程意外终止异常
     */
    void consume() throws InterruptedException;

}
package com.github.xuchengen.concurrent;

/**
 * 抽象生产者
 * 作者:徐承恩
 * 邮箱:xuchengen@gmail.com
 * 日期:2019/12/11
 */
public abstract class AbsProducer implements Producer, Runnable {

    @Override
    public void run() {
        while (true) {
            try {
                produce();
            } catch (InterruptedException e) {
                e.printStackTrace();
                break;
            }
        }
    }
}
package com.github.xuchengen.concurrent;

/**
 * 抽象生产者
 * 作者:徐承恩
 * 邮箱:xuchengen@gmail.com
 * 日期:2019/12/11
 */
public abstract class AbsConsumer implements Consumer, Runnable {

    @Override
    public void run() {
        while (true) {
            try {
                consume();
            } catch (InterruptedException e) {
                e.printStackTrace();
                break;
            }
        }
    }

}

不同的模型实现中,生产者、消费者的具体实现也不同,所以需要为模型定义抽象工厂方法:

package com.github.xuchengen.concurrent;

/**
 * 模型
 * 作者:徐承恩
 * 邮箱:xuchengen@gmail.com
 * 日期:2019/12/11
 */
public interface Model {

    /**
     * 实例化一个消费者
     *
     * @return Runnable
     */
    Runnable newRunnableConsumer();

    /**
     * 实例化一个生产者
     *
     * @return Runnable
     */
    Runnable newRunnableProducer();

}

我们将Task作为生产和消费的单位:

package com.github.xuchengen.concurrent;

/**
 * 任务
 * 作者:徐承恩
 * 邮箱:xuchengen@gmail.com
 * 日期:2019/12/11
 */
public class Task {

    /**
     * 任务号
     */
    public int no;

    public Task(int no) {
        this.no = no;
    }
}

如果需求还不明确(这符合大部分工程工作的实际情况),建议边实现边抽象,不要“面向未来编程”

Java中finally和return优先级

作为一名Java开发者,拥有扎实的Java基础才能立于不败之地,比如面试或者被面试等等情况。在某些情况下Java的语法极具迷惑性也就是所谓的“坑”比如finally和return语句最终返回谁的结果?,那么本篇将总结一下Java中finally和return的优先级。

代码

package com.github.xuchengen.other;

/**
 * 最终返回实例
 * 作者:徐承恩
 * 邮箱:xuchengen@gmail.com
 * 日期:2019/11/4
 */
public class FinallyReturnExample {

    public static void main(String[] args) {

        System.out.println(finallyReturn1());

        System.out.println(finallyReturn2());
    }

    private static String finallyReturn1() {
        try {
            throw new RuntimeException();
        } catch (Exception e) {
            return "catch";
        } finally {
            return "finally";
        }
    }

    private static int finallyReturn2() {
        int a = 1;
        try {
            return a;
        } catch (Exception e) {
            a = -1;
        } finally {
            a = 30;
            System.out.println(a);
        }
        return a;
    }
}

finallyReturn1函数最终执行结果为:finally

finallyReturn2函数最终执行结果为:打印30,函数返回1

上述问题的本质就是在try、catch、finally中都有return语句时,执行代码的顺序是怎么样的,是根据哪个值来进行返回呢?

我们知道在处理异常时,finally中的代码是必定要执行的。这是由Java编译器决定的,在编译的时候将try模块的代码与finally模块的代码合并在一起,将catch模块的代码与finally模块的代码合并在一起,这是毫无疑问的。
这样,当finally模块有return那么将会执行finally中的return返回函数的结果,无论try、catch,还是函数体有没有return语句。所以该位置的return的优先级是最高的。

那么当finally没有return时是如何返回的呢?
这时在执行完try中的模块后,有return语句,实际不会真正的return,即只是会计算return中的表达式,之后将计算的结果保存在一个临时栈中,接着执行finally中的语句,最后才会从临时栈中取出之前的结果返回。
所以,函数的返回值是1而非30。

总体来说,return语句的位置有如下几种。

public static int getNumer() {
    try {            
        return a;
    } catch (Exception e) {
        return b;
    } finally {
       return c;
    }
    return d;
}

当无异常抛出时,返回的优先级如下:c>a>d

当然,如果c存在,d是不可达代码,编译会错误的,如下:

Exception in thread "main" java.lang.Error: Unresolved compilation problem: Unreachable code

当有异常抛出时,返回的优先级如下:c>b>d

总之,大家记住,finally块中的return优先级最高,而函数体中的return的优先级最低就好了。

Java窗口编程仿网页分页原理实现

仿照未必是一件坏事,先要学会仿照,接着理解他,吃透他,等你懂了一定原理后,发现以前的东西不怎么够好,需要改进,那么通过你的改进,使你的软件用户体验更好,我想这应该就是微创新吧。

这个例子的源码是CSDN的一位开发者所分享且一直珍藏在我的浏览器收藏夹多年,由于历史原因现在CSDN已经找不到原文。本着开源分享互助的精神我将源码完善再次公布如下:

package com.github.xuchengen.windows;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

/**
 * 仿网页分页
 * 作者:徐承恩
 * 邮箱:xuchengen@gmail.com
 * 日期:2019/10/30
 */
public class Pagination extends JFrame {

    /**
     * 总的页码数
     */
    private int pageTotal = 35;

    /**
     * 当前页
     */
    private int currentPage = 1;

    /**
     * 存放一系列页码按钮,默认流布局
     */
    private JPanel pageBtns = new JPanel();

    /**
     * 无参数构造方法
     */
    private Pagination() {
        //设置窗口标题
        this.setTitle("仿网页分页By徐承恩");
        //设置JFrame的布局
        this.setLayout(new FlowLayout(FlowLayout.LEFT, 20, 20));
        //设置窗口可拉伸
        this.setResizable(true);
        //档点击叉叉时,窗口可关闭
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        //默认显示第一页 ,设置页码布局
        setPageBottom(currentPage);
        //自适应子控件宽和高度度来调整窗口大小
        this.pack();
        //窗口居中
        this.setLocationRelativeTo(null);
        //窗口可见
        this.setVisible(true);
    }

    /**
     * 设置页码按钮
     *
     * @param currentPage 当前页码
     */
    private void setPageBottom(int currentPage) {

        pageBtns.removeAll();

        if (currentPage <= 0 || currentPage > pageTotal) {
            return;
        }

        //这个是显示数字按钮的总个数,不包括首页 尾页等其他按钮
        int countNumBtn = 9; // 首页 1 2 3 4 5 6 7 8 9 尾页

        int half = countNumBtn / 2;

        int startNum = 0;

        int endNum = 0;

        JButton btnFistPage = new JButton("首页");
        btnFistPage.setActionCommand("首页");
        btnFistPage.setToolTipText("首页");
        btnFistPage.setToolTipText("首页");
        btnFistPage.addActionListener(new BottomPageButtonAction());

        JButton btnLastPage = new JButton("末页");
        btnLastPage.setActionCommand("末页");
        btnLastPage.addActionListener(new BottomPageButtonAction());
        btnLastPage.setToolTipText("共" + pageTotal + "页");

        Container con = this.getContentPane();
        con.invalidate();

        pageBtns.add(btnFistPage);

        if (currentPage != 1) {
            JButton btnPrePage = new JButton("上一页");
            btnPrePage.setActionCommand("上一页");
            btnPrePage.setToolTipText("上一页是第" + (Math.max(currentPage - 1, 1)) + "页");
            btnPrePage.addActionListener(new BottomPageButtonAction());
            pageBtns.add(btnPrePage);
        }

        // 下面开始计算从左至右数字键(JButton)上的text
        int minBtnNum = currentPage - half;
        int maxBtnNum = currentPage + half;
        if (minBtnNum > 0 && maxBtnNum <= pageTotal) {
            startNum = minBtnNum;
            endNum = maxBtnNum;
        } else if (minBtnNum <= 0) {
            startNum = 1;
            endNum = Math.min(countNumBtn, pageTotal);
        } else {
            startNum = pageTotal > countNumBtn ? pageTotal - (countNumBtn - 1) : 1;
            endNum = pageTotal;
        }

        for (int i = startNum; i <= endNum; i++) {
            JButton btn = new JButton();
            btn.addActionListener(new BottomPageButtonAction(i));
            btn.setActionCommand("数字");
            btn.setToolTipText("第" + i + "页");
            btn.setText(i + "");
            if (i == currentPage) {
                btn.setBackground(Color.red);
            } else {
                btn.setBackground(Color.white);
            }
            pageBtns.add(btn);
        }

        if (currentPage != pageTotal) {
            JButton btnNextPage = new JButton("下一页");
            btnNextPage.setActionCommand("下一页");
            btnNextPage.setToolTipText("下一页是第" + (Math.min(currentPage + 1, pageTotal)) + "页");
            btnNextPage.addActionListener(new BottomPageButtonAction());
            pageBtns.add(btnNextPage);
        }
        pageBtns.add(btnLastPage);
        con.validate();
        this.add(pageBtns);
    }

    //页码事件处理
    class BottomPageButtonAction implements ActionListener {

        int btnNumText = 0;

        BottomPageButtonAction() {

        }

        BottomPageButtonAction(int num) {
            btnNumText = num;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            String command = e.getActionCommand();
            System.out.println(command);
            switch (command) {
                case "首页":  //
                    setPageBottom(1); // 首页就是第一页,所以直接传个1

                    currentPage = 1;
                    break;
                case "上一页":
                    setPageBottom(currentPage - 1 >= 1 ? --currentPage : 1);
                    break;
                case "下一页":
                    setPageBottom(currentPage + 1 <= pageTotal ? ++currentPage : pageTotal);

                    break;
                case "末页":
                    setPageBottom(pageTotal); // 末页是最后的页数

                    currentPage = pageTotal;
                    break;
                case "数字":
                    setPageBottom(btnNumText);
                    currentPage = btnNumText;
                    break;
            }
            //自适应子控件宽和高度度来调整窗口大小
            pack();
            System.out.println("当前是第 " + currentPage + "页");
        }

    }

    public static void main(String[] args) {
        new Pagination();
    }

}

JAVA仿网页分页原理

Java中八种数据类型所占字节计算

Java中有八种基本数据类型,分别为:byte、short、int、long、float、double、char、boolean。
这八种基本类型都有对应的包装类,分别为:Byte、Short、Integer、Long、Float、Double、Character、Boolean。

面试时时常会闻到这八种基本类型及其包装类,而且各种基本类型所占的字节数即使记不住、能用代码实现也是可以的。所以给出基本数据类型所占字节的代码。以供大家参考。

    @Test
    public void t1() {
        //一个字节占8个二进制位

        System.out.println("byte的二进制位数:" + Byte.SIZE);
        System.out.println("byte所占用的字节数:" + Byte.SIZE / 8);

        System.out.println("short的二进制位数:" + Short.SIZE);
        System.out.println("short所占用的字节数:" + Short.SIZE / 8);

        System.out.println("int的二进制位数:" + Integer.SIZE);
        System.out.println("int所占用的字节数:" + Integer.SIZE / 8);

        System.out.println("long的二进制位数:" + Long.SIZE);
        System.out.println("long所占用的字节数:" + Long.SIZE / 8);

        System.out.println("float的二进制位数:" + Float.SIZE);
        System.out.println("float所占用的字节数:" + Float.SIZE / 8);

        System.out.println("double的二进制位数:" + Double.SIZE);
        System.out.println("double所占用的字节数:" + Double.SIZE / 8);

        System.out.println("char的二进制位数:" + Character.SIZE);
        System.out.println("char所占用的字节数:" + Character.SIZE / 8);
    }
byte的二进制位数:8
byte所占用的字节数:1
short的二进制位数:16
short所占用的字节数:2
int的二进制位数:32
int所占用的字节数:4
long的二进制位数:64
long所占用的字节数:8
float的二进制位数:32
float所占用的字节数:4
double的二进制位数:64
double所占用的字节数:8
char的二进制位数:16
char所占用的字节数:2

所以就可以理解基本数据类型的大小关系:

 【byte(1Byte) < char(2Byte) < short(2Byte) < int(4Byte) < float(4Byte) <double(8Byte) < long(8Byte)】 这个排序是错误的!

正确的是:在Java中整型、实型、字符型被视为简单数据类型,这些类型由低级到高级分别为:

(byte,short,char)–int–long–float–double

注意,整数比浮点数低级。低级到高级可以自动转换。而高级到低级需要用代码强制转换,不强转会编译错误。

总结

  1. 位:”位(bit)”是电子计算机中最小的数据单位。每一位的状态只能是0或1。
  2. 字节:8个二进制位构成1个”字节(Byte)”,它是存储空间的基本计量单位。
  3. 1个字节可以储存1个英文字母或者半个汉字,换句话说,1个汉字占据2个字节的存储空间。
  4. 字:”字”由若干个字节构成,字的位数叫做字长,不同档次的机器有不同的字长。
  5. KB:K在二进制中表示1024,也就是2的10次 方。1KB表示1K个Byte,也就是1024个字节。

例如一台8位机,它的1个字就等于1个字节,字长为8位。如果是一台16位机,那么,它的1个字就由2个字节构成,字长为16位。

字是计算机进行数据处理和运算的单位。

【习题】

一个函数定义的返回值是float,它不能在return语句中返回的值的类型是(  )?

A.char              B.float        C.long         D.double

答案:D

double比float高级,long比float低级

TKMapper通用Mapper生成主键策略的几种方式

通用 Mapper 是一个可以实现任意 MyBatis 通用方法的框架,项目提供了常规的增删改查操作以及Example 相关的单表操作。通用 Mapper 是为了解决 MyBatis 使用中 90% 的基本操作,使用它可以很方便的进行开发,可以节省开发人员大量的时间。

一、Mybatis基于Maven插件快速生成Java模型以及XML

<build>
    <plugins>
        <!-- mybatis自动生成插件 -->
        <plugin>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-maven-plugin</artifactId>
            <version>1.3.2</version>
            <dependencies>
                <dependency>
                    <groupId>mysql</groupId>
                    <artifactId>mysql-connector-java</artifactId>
                    <version>5.1.47</version>
                </dependency>
                <dependency>
                    <groupId>tk.mybatis</groupId>
                    <artifactId>mapper</artifactId>
                    <version>3.4.4</version>
                </dependency>
            </dependencies>
            <configuration>
                <!--配置文件的路径-->
                <configurationFile>src/main/resources/AGeneratorConfig.xml</configurationFile>
                <overwrite>true</overwrite>
            </configuration>
        </plugin>
    </plugins>
</build>

AGeneratorConfig.xml这里以A打头的命名方式主要是方便快速找到自动生成配置文件。

一、自增主键方式一

<table tableName="t_car_mortgage" domainObjectName="CarMortgageDO"
       enableSelectByExample="false"
       enableDeleteByExample="false"
       enableCountByExample="false"
       enableUpdateByExample="false">
    <generatedKey column="id" sqlStatement="MySql" identity="true"/>
</table>

自增主键策略生成的Java模型例子:

@Table(name = "`t_car_mortgage`")
public class CarMortgageDO {
    /**
     * 主键
     */
    @Id
    @Column(name = "`id`")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
}

二、自增主键方式二

在项目开发中遇到过主键生成重复报错,将Java模型注解改成JDBC如下这种方式可解决。猜测大概是不支持bigint这样的字段类型吧。

@Table(name = "`t_car_mortgage`")
public class CarMortgageDO {
    /**
     * 主键
     */
    @Id
    @Column(name = "`id`")
    @GeneratedValue(generator = "JDBC")
    private Long id;
}

三、UUID主键策略

<table tableName="T_CUSTOMER_INFO" domainObjectName="CustomerInfoDO"
       enableSelectByExample="false"
       enableDeleteByExample="false"
       enableCountByExample="false"
       enableUpdateByExample="false">
    <generatedKey column="customer_id" sqlStatement="select upper(replace(uuid(),'-',''))" identity="false" type="pre"/>
</table>

UUID主键策略生成的Java模型例子:

@Table(name = "`t_customer_info`")
public class CustomerInfoDO {
    /**
     * 客户id
     */
    @Id
    @Column(name = "`customer_id`")
    @GeneratedValue(strategy = GenerationType.IDENTITY, generator = "select upper(replace(uuid(),'-',''))")
    private String customerId;
}

 

Java中Thread类中State枚举类定义与说明

NEW状态表示刚刚创建的线程,这种线程还没开始执行。等到线程的start()方法调用时。才表示线程开始执行。当现场执行时,处于RUNNABLE状态,表示线程所需的一切资源都已经准备好了。如果线程在执行过程中遇到了synchronized同步块,就会进入BLOCKED阻塞状态,这时线程就会暂停执行,直到获得请求的锁。WAITINGTIMED_WAITING都表示等待状态,它们的区别是WAITING会进入一个无时间限制的等待,TIMED_WAITING会进行一个有时限的等待。那等待的线程究竟在等什么呢?一般来说,WAITING的线程正是在等待一些特殊的事件。比如,通过wait()方法等待的线程在等待notify()方法,而通过join()方法等待的线程则会等待目标线程的终止。一旦等到了期望的事件,线程就会再次执行,进入RUNNABLE状态。当线程执行完毕后,则进入TERMINATED状态,标识结束。

public enum State {
    /**
     * Thread state for a thread which has not yet started.
     */
    NEW,

    /**
     * Thread state for a runnable thread.  A thread in the runnable
     * state is executing in the Java virtual machine but it may
     * be waiting for other resources from the operating system
     * such as processor.
     */
    RUNNABLE,

    /**
     * Thread state for a thread blocked waiting for a monitor lock.
     * A thread in the blocked state is waiting for a monitor lock
     * to enter a synchronized block/method or
     * reenter a synchronized block/method after calling
     * {@link Object#wait() Object.wait}.
     */
    BLOCKED,

    /**
     * Thread state for a waiting thread.
     * A thread is in the waiting state due to calling one of the
     * following methods:
     * <ul>
     * <li>{@link Object#wait() Object.wait} with no timeout</li>
     * <li>{@link #join() Thread.join} with no timeout</li>
     * <li>{@link LockSupport#park() LockSupport.park}</li>
     * </ul>
     *
     * <p>A thread in the waiting state is waiting for another thread to
     * perform a particular action.
     * <p>
     * For example, a thread that has called <tt>Object.wait()</tt>
     * on an object is waiting for another thread to call
     * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
     * that object. A thread that has called <tt>Thread.join()</tt>
     * is waiting for a specified thread to terminate.
     */
    WAITING,

    /**
     * Thread state for a waiting thread with a specified waiting time.
     * A thread is in the timed waiting state due to calling one of
     * the following methods with a specified positive waiting time:
     * <ul>
     * <li>{@link #sleep Thread.sleep}</li>
     * <li>{@link Object#wait(long) Object.wait} with timeout</li>
     * <li>{@link #join(long) Thread.join} with timeout</li>
     * <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
     * <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
     * </ul>
     */
    TIMED_WAITING,

    /**
     * Thread state for a terminated thread.
     * The thread has completed execution.
     */
    TERMINATED;
}

注意:从NEW状态发出后,线程不能再回到NEW状态,同理,处于TERMINATED的线程也不能再回到RUNNABLE状态。

一句话介绍synchronized

JVM会自动通过monitor来加锁和解锁,保证了同时只有一个线程可以执行指定代码,从而保证了线程的安全,同时具有可重入和不可中断的性质。

包装类对象之间值的比较全部采用equals方法比较

对于 Integer var = ? 在-128 至 127 范围内的赋值,Integer 对象是在 IntegerCache.cache 产生,会复用已有对象,这个区间内的 Integer 值可以直接使用==进行 判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑, 推荐使用 equals 方法进行判断。

Java多线程的三个核心思想

  1. 一把锁只能同时被一个线程所持有,没有拿到锁的线程只能等待。
  2. 每个实例都对应有自己的一把锁,不同实例互不影响。注意:当锁对象是*.class以及synchronized修饰的static方法时所有对象都共用同一把锁。
  3. 无论方法是正常执行完毕还是抛出异常,都会释放锁。