精华内容
下载资源
问答
  • Java IOJava 中传统的 IO 包基于流模型实现,交互方式为同步、阻塞,当发生读取或写入操作时,线程会阻塞在此,直到操作完成。编码时采用这种方式虽然源码较直观易维护,但容易产生应用性能下降问题,且 IO 效率及其...

    fdcffdbfced9ab07b21c836b40809eb9.png

    作者:MobMsg,资深全端工程师一枚,架构师社区合伙人!


    Java IO

    Java 中传统的 IO 包基于流模型实现,交互方式为同步、阻塞,当发生读取或写入操作时,线程会阻塞在此,直到操作完成。编码时采用这种方式虽然源码较直观易维护,但容易产生应用性能下降问题,且 IO 效率及其拓展性存在较大局限

    ecddac6d67c669adb20855e661400bbc.png

    InputStream/OutputStream vs Reader/Writer:IO 中的输入/输出流(InputStream/OutputStream)用于读写字节,直接操作文件本身。Reader/Writer 用于操作字符,处理的是如文本文件内包含的信息内容

    使用 io 相关工具类,切记进行资源释放,否则资源将被占用且无法释放,释放的方式可选 try-finall 机制。io 除了用于对文件的操作,网络编程中的 Socket、ServerSocket、HttpURLConnection 也是 io 操作,因为网络通信也属 io 行为


    Java NIO

    Java 1.4 开始引入 NIO 框架,提供了 Channel(通道)、Selector(IO复用器/选择器)、Buffer(缓冲区),可构建多路复用、同步非阻塞的 IO 程序,同时在数据操作方式方面更接近操作系统底层所以性能更高

    1ea327784055660640672de21d486983.png

    在 Java 7 中,NIO 再次改进,引入异步非阻塞 IO 方式,基于事件和回调机制,也就是处理开始时不阻塞,处理完成后系统通知对应线程继续后续工作。这种方式也称为 NIO 2 或 AIO(Asynchronous IO)


    NIO Channel、Buffer、Selector

    在 IO 中数据操作基于字节流或字符流,在 NIO 中数据基于 Channel 和 Buffer 进行操作

    Channel(通道):在缓冲区和位于通道另一侧的服务之间进行数据传输,支持单向或双向传输,支持阻塞或非阻塞模式

    Buffer(缓冲区):高效数据容器。本质上是一块内存区,用于数据的读写,NIO Buffer 将其包裹并提供开发时常用的接口,便于数据操作

    Selector(IO复用器/选择器):多路复用的重要组成部分,检查一个或多个Channel(通道)是否是可读、写状态,实现单线程管理多Channel(通道),优于使用多线程或线程池产生的系统资源开销

    349a2df45c46e66603389d5c25e4db19.png

    NIO Channel

    Channel 在缓冲区和位于通道另一侧的服务之间进行数据传输,支持单向或双向传输,支持阻塞或非阻塞模式

    b3ec4feea973342a154a78d80ec7d0cd.png

    几种主要的 Channel 类型(FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel)已覆盖了我们日常开发常见场景,如不同网络传输IO、文件IO。Channel 中的数据支持以异步方式存、取至 Buffer


    NIO Buffer

    Buffer 是高效数据容器。它本质上是一块内存区,基于数组实现存储,用于数据的读写,NIO Buffer 将其包裹并提供开发时常用的接口,便于数据操作

    9b7505b4000a567b2914b565f2e53140.png

    Client 向 Buffer 写入数据后,调用 flip() 将 Buffer 由写模式更改为读模式,此时 Channel 可以读取 Buffer 内数据,读取完成后可调用 clear()compact() 重置缓冲区并允许数据写入


    NIO Selector

    Selector 是 NIO 多路复用的重要组成部分。它负责检查一个或多个Channel(通道)是否是可读、写状态,实现单线程管理多 Channel(通道),优于使用多线程或线程池产生的系统资源开销

    eceaea08ae7c71170d00e5814624ac42.png

    向 Selector 注册 Channel 时可指定的类型有四种:OP_READOP_WRITEOP_CONNECTOP_ACCEPT

    // 读
    public static final int OP_READ = 1 <0; 

    // 写
    public static final int OP_WRITE = 1 <2; 

    // 连接;SocketChannel 独有,其它类型 channel 不支持
    public static final int OP_CONNECT = 1 <3; 

     // 接收;ServerSocketChannel 独有,其它类型 channel 不支持
    public static final int OP_ACCEPT = 1 <4;

    注册时可以为同一通道注册多个感兴趣的事件,互相之间使用操作符 * | * 位或连接,如:

    Channel.register(Selector, SelectionKey.OP_READ|SelectionKey.OP_WRITE);

    当 Channel(通道)  注册至 Selector 内后,便会产生一个对应的 SelectionKey,存储与此 Channel 相关的数据


    NIO SelectionKey

    SelectionKey 是 channel 在 Selector 内注册的标识,每个 channel 对应一个 SelectionKey, SelectionKey 内包含有如下属性:

    interest Set:兴趣集合,当前 channel 感兴趣的操作ready Set:就绪集合,此 SelectionKey  已经准备就绪的操作集合Channel:通道,获取此 SelectionKey 对应的 channelSelector:选择器,管理此 channel 的 SelectorAttach:附加对象,向 SelectionKey 中添加更多的信息,方便之后的数据操作判断或获取


    FileChannel 主要方法

    java.io.RandomAccessFile.getChannel()

    FileChannel 类型通道实例从输入流中获取,如:FileInputStream.getChannel()RandomAccessFile.getChannel()

    java.nio.channels.FileChannel.read(buffer)

    从 FileChannel 类通道中读取数据并存入指定的 buffer 中

    java.nio.channels.FileChannel.write(buffer)

    从指定的 buffer 中读取数据并向 FileChannel 类通道写入

    java.nio.channels.FileChannel.position()

    获取当前 position 位置

    java.nio.channels.FileChannel.position(long newPosition)

    从指定位置开始写入操作

    java.nio.channels.FileChannel.truncate(long size)

    指定长度并截取文件

    java.nio.channels.spi.AbstractInterruptibleChannel.close()

    关闭此通道


    DatagramChannel 主要方法

    java.nio.channels.DatagramChannel.open()

    打开一个 DatagramChannel 类型通道

    java.nio.channels.DatagramChannel.socket().bind(SocketAddress addr)

    接收指定端口中的UDP协议数据

    java.nio.channels.DatagramChannel.receive(buffer)

    将数据写入到指定 buffer 中

    java.nio.channels.DatagramChannel.send(ByteBuffer src, SocketAddress target)

    将指定的 buffer 内的数据发送给指定的 IP地址+端口号

    java.nio.channels.spi.AbstractInterruptibleChannel.close()

    关闭此通道


    SocketChannel 主要方法

    java.nio.channels.SocketChannel.open()

    打开一个 SocketChannel 类型通道

    java.nio.channels.SocketChannel.connect(SocketAddress remote)

    通过 Socket 方式连接至指定的IP地址+端口号

    java.nio.channels.SocketChannel.read(buffet)

    从 SocketChannel 类通道中读取数据并存入指定的 buffer 中

    java.nio.channels.SocketChannel.write(buffer)

    从指定的 buffer 中读取数据并向 SocketChannel 类通道写入

    java.nio.channels.spi.AbstractSelectableChannel.configureBlocking(boolean block)

    设置阻塞模式,false 为非阻塞

    java.nio.channels.spi.AbstractInterruptibleChannel.close()

    关闭此通道


    ServerSocketChannel 主要方法

    java.nio.channels.ServerSocketChannel.open()

    打开一个 ServerSocketChannel 类型通道

    java.nio.channels.ServerSocketChannel.bind(SocketAddress local)

    监听指定端口下的TCP连接

    ServerSocketChannel.accept()

    监听新连接。通常用 while(true){} 方式循环监听,获取到新 channel 后根据其事件做对应操作

    java.nio.channels.spi.AbstractSelectableChannel.configureBlocking(boolean block)

    设置阻塞模式,false 为非阻塞

    java.nio.channels.spi.AbstractInterruptibleChannel.close()

    关闭此通道


    Buffer 主要方法

    java.nio.****Buffer.allocate(int capacity)

    创建缓冲区并设定容量

    java.nio.****Buffer..fwrap(byte[] array)

    根据存入数组大小新建/更新缓冲区并重设容量为 array.length

    java.nio.Buffer.flip()

    将 Buffer 由写模式更改为读模式

    java.nio.Buffer.clear()java.nio.Buffer.compact()

    两者均为允许数据写入(由读模式更改为写模式)并重置缓冲区标识(capacitypositionlimit),但对待标识参数的具体处理不同,决定了后续操作对数据的影响也不同

    capacity 表示缓冲区的最大容量。position 在读模式下指定读取开始位置的索引,由写模式切换过来时 position 会被置0。在写模式下指定写入元素下标,最大可用下标为 capacity -1,可用下标从 0 开始。limit 在读模式下代表本缓冲区内最多可读数据量。写模式下代表本缓冲区最多可用空间

    clear()  与 compact() 的区别在于对 Buffer 内现存数据的后续使用造成的影响不同。clear() 会做如下操作: position = 0; limit = capacity; mark = -1; 也就是说数据虽未被删除,但当我们之后再次写入时,将不再关心是否保留它们而直接覆盖。而 compact() 会把所有未读数据拷贝至起始处,将position设为最后一个未读元素后,将limit设置为capacity,也就是说再次写入数据时不会覆盖未读数据,这些数据将被继续缓存并等待之后的使用


    Selector 主要方法

    java.nio.channels.Selector.select()

    准备一组已准备好进行 I/O 操作的 channel 。以阻塞线程的方式,直到返回至少一个符合要求的 channel

    java.nio.channels.Selector.select(long timeout)

    准备一组已准备好进行 I/O 操作的 channel 。以阻塞线程的方式,直到返回至少一个符合要求的 channel 或者给定的超时时间到期

    java.nio.channels.Selector.selectNow()

    准备一组已准备好进行 I/O 操作的 channel 。以非阻塞线程的方式,如果没有符合要求的通道,则直接返回 0

    java.nio.channels.Selector.selectedKeys()

    准备好可进行 I/O 操作的 channel 后,调用此方法获取已就绪的 channel,之后遍历并判断 channel 对应的事件即可


    SelectionKey 主要方法

    java.nio.channels.SelectionKey.channel();

    返回当前 SelectionKey 对应的 Channel ,即使其已关闭,也同样返回

    java.nio.channels.SelectionKey.selector();

    返回管理此 SelectionKey 的 Selectoe,即使其已关闭,也同样返回 Selectoe

    java.nio.channels.SelectionKey.isValid();

    返回当前 SelectionKey 是否有效的状态。SelectionKey  在创建时有效并持续保持,在被取消cancel()或删除removeKey(key)后变为无效,在 Channel 或 Seelctor 被关闭后同样变为无效

    java.nio.channels.SelectionKey.cancel();

    取消此 key 对应的 channel 在 selector 内取消。valid 也将更新为 false,此 key 将被添加至 cancelledkey 集合内

    java.nio.channels.SelectionKey.interestOps();

    获取 SelectionKey 中包含的 interest set, 存储的是我们设定的事件

    java.nio.channels.SelectionKey.interestOps(int ops)

    将此 key 对应的interst设置为指定值,此操作会对 ops 和 channel.validOps 进行校验,如果此ops不被当前channel支持,将抛出异常

    java.nio.channels.SelectionKey.readyOps();

    获取 SelectionKey 中包含的 ready set,存储的是准备就绪的事件。每次 select() 时,选择器都会对 ready set 进行更新,外部程序无法修改此集合.

    java.nio.channels.SelectionKey.isReadable();

    检测此键是否为"read"事件,等效于:k.,readyOps() & OP_READ != 0;还有isWritable(),isConnectable(),isAcceptable()

    java.nio.channels.SelectionKey.attach(Object ob)

    添加附件。若需要向 SelectionKey 添加更多数据信息,方便之后的操作,可通过 attach(Object ob) 或 register channel 时存入

    java.nio.channels.SelectionKey.attachment();

    获取附件。附件可在 Channe 生命周期中共享,但不可作为 socket 数据实现网络传输


    应用源码实例

    服务端

    /**
     * NIO Server
     * @author liyongli 20191029
     * */

    public class NIOServerDemo {

        // IO复用器丨选择器
        private Selector NIOServerSelector;

        // 数据通道
        private ServerSocketChannel NIOServerSocketChannel;

        public static void main(String[] args{
            // 先运行本类代码,再运行客户端代码
            NIOServerDemo NIOServer = new NIOServerDemo();

            // 初始化服务器配置
            NIOServer.initServerSocketChannel("localhost"8081);

            // 启动监听
            NIOServer.startNIOServerSelectorListener();
        }

        /**
         * 注册 Channel
         * @param hostname 主机地址
         * @param port 端口号
         * */

        private void initServerSocketChannel(String hostname, int port){
            try {
                // 初始化IO复用器丨选择器
                NIOServerSelector = Selector.open();

                // 打开通道
                NIOServerSocketChannel = ServerSocketChannel.open();

                // 调整模式为非阻塞
                NIOServerSocketChannel.configureBlocking(false);

                // 设置端口
                NIOServerSocketChannel.socket().bind(new InetSocketAddress(hostname, port));

                // 注册此通道
                NIOServerSocketChannel.register(NIOServerSelector, SelectionKey.OP_ACCEPT);
                System.out.println("服务端准备就绪");

            } catch (IOException e) {
                System.out.println("服务端初始化失败");
                e.printStackTrace();
            }
        }

        /**
         * 启动 Selector
         * */

        private void startNIOServerSelectorListener(){
            while(true){
                try {
                    // 选中通道
                    NIOServerSelector.select();

                    // 获取所有 key
                    Iterator NIOServerSelectorIterator = NIOServerSelector.selectedKeys().iterator();while(NIOServerSelectorIterator.hasNext()){// 获取 key
                        SelectionKey selectionKey = NIOServerSelectorIterator.next();// 判断当前 channel 是否可接收 socket 连接if(selectionKey.isAcceptable()){// 复用
                            SocketChannel socketChannel = NIOServerSocketChannel.accept();
                            socketChannel.configureBlocking(false);
                            socketChannel.register(NIOServerSelector, SelectionKey.OP_READ);// 判断当前通道是否可读取
                        }else if(selectionKey.isReadable()){
                            callClient(selectionKey);
                        }// 删除已处理完成的 key 
                        NIOServerSelectorIterator.remove();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }/**
         * 自动回复 Client
         * */
    private void callClient(SelectionKey selectionKey){try {// 获取对应通道
                SocketChannel socketChannel = (SocketChannel) selectionKey.channel();// 新建缓冲区
                ByteBuffer byteBuffer = ByteBuffer.allocate(1024);// 读取通道数据并存入缓冲区int index = socketChannel.read(byteBuffer);// 若通道内有数据if(index != -1){
                    System.out.println("服务端接收:" + new String(byteBuffer.array()));// 自动回复(此处可添加对应业务逻辑)
                    socketChannel.write(ByteBuffer.wrap("hello client,im waiting for you!".getBytes()));
                    System.out.println("服务端回复:" + "hello client,im waiting for you!");
                }else{// 通道内无数据
                    socketChannel.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    客户端

    /**
     * NIO Client
     * @author liyongli 20191029
     * */

    public class NIOClientDemo {

        // 定义通道
        private SocketChannel socketChannel;

        public static void main(String[] args){
            // 先运行服务端代码,再运行本类
            NIOClientDemo NIOClient = new NIOClientDemo();

            // 初始化连接服务器配置
            NIOClient.initClientChannel("localhost"8081);

            // 发送消息
            NIOClient.callServer("hello server,what are you doing?");
        }

        /**
         * 初始化客户端 NIO Channel
         * */

        public void initClientChannel(String hostname, int port){
            try {
                // 初始化 socket
                InetSocketAddress inetSocketAddress = new InetSocketAddress(hostname, port);

                // 建立通道
                socketChannel = SocketChannel.open(inetSocketAddress);
                System.out.println("客户端准备就绪");

            } catch (IOException e) {
                System.out.println("初始化客户端失败");
                e.printStackTrace();
            }
        }

        /**
         * 通信 Server
         * */

        public void callServer(String callStr){
            // 将字符串转换为 byte 数组,便于稍后传输
            byte[] requestByte = new String(callStr).getBytes();

            // 创建一个1024 容量的 ByteBuffer 
            ByteBuffer byteBuffer = ByteBuffer.wrap(requestByte);
            System.out.println("客户端发送:" + new String(byteBuffer.array()));

            if(null != socketChannel){
                try {
                    // 向通道写入数据
                    socketChannel.write(byteBuffer);

                    // 清空缓冲区(数据并未被删除,但位置、标记、限制被重置)
                    byteBuffer.clear();

                    // 读取被服务器更新的数据
                    socketChannel.read(byteBuffer);
                    System.out.println("客户端接收:" + new String(byteBuffer.array()));

                    // 关闭通道
                    socketChannel.close();

                } catch (IOException e) {
                    System.out.println("通信出错");
                    e.printStackTrace();
                }
            }else{
                System.out.println("请初始化客户端");
            }
        }
    }

    运行结果

    服务端准备就绪
    服务端接收:hello server,what are you doing?
    服务端回复:hello client,im waiting for you!
    客户端准备就绪
    客户端发送:hello server,what are you doing?
    客户端接收:hello client,im waiting for you!

    本篇将持续更新 Java NIO 相关知识,一起查漏补缺学个痛快!欢迎点赞留香丨留言鼓励丨指出不足!

    长按订阅更多精彩▼

    26527148d75296b69433aa88f914a29e.png

    521ea71fefafd01d78d95d132946b248.gif

    展开全文
  • Object 相关概念Object 是 java 中的顶级父类,它是所有类的超类,所有对象以及数组均会实现这个类提供的方法JVM 在编译源码过程中,遇到没有继承 Object 的对象时,编译器会指定默认父类 Object接口没有继承顶级...

    6c8f2f8102fea55b8e2bef9dadc6c750.png

    作者:MobMsg,资深全端工程师一枚,架构师社区合伙人!

    Object 相关概念

    Object 是 java 中的顶级父类,它是所有类的超类,所有对象以及数组均会实现这个类提供的方法

    JVM 在编译源码过程中,遇到没有继承 Object 的对象时,编译器会指定默认父类 Object

    接口没有继承顶级父类,但会隐式的声明一套和 Object 中的方法签名完全一样的方法,这也就符合万物皆对象的面向对象思想,任何对象直接或间接的跟 Object 对象有关


    Object 类源码中的关键方法

    public class Object {

        private static native void registerNatives();
        static {
            registerNatives();
        }
        ···
    }

    registerNatives():注册本地方法

    它会注册除 registerNatives外的所有本地方法。当 java 程序需要调用本地方法时,jvm 会在加载的动态文件里定位并链接该本地方法,从而得以执行此方法。

    在类被加载时就调用 registerNatives() 的用意是此时是程序主动将本地方法链接到调用方,当 java 程序需要调用本地方法时可直接调用,省去了jvm再去定位并链接的这一步,这样做的好处是:

    1. 更加方便且提高了执行效率

    2. 当本地方法在程序运行中有更新,调用 registerNatives() 可及时实现更新

    3. Java程序需要调用一个本地应用提供的方法时,因为虚拟机只会检索本地动态库,因而虚拟机是无法定位到本地方法实现的,这个时候就只能使用 registerNatives() 进行主动链接

    4. 通过 registerNatives() 在定义本地方法的实现时,可以不遵守 JNI 的命名规范

        ···
        public final native Class> getClass();

    getClass():返回此对象的运行时类

    返回值是 Class 类型,通过返回的 Class 对象我们可以获取目标类中包含的所有方法、所有变量、构造函数等

        ···
        public native int hashCode();

    hashCode():返回此对象的存储地址。主要用于判断对象是否相同,可提高查询、存储操作的效率

    equals():比较。但它有不同的提供来源

        // 这是 Object 类提供的 equals()
        ···
        public boolean equals(Object obj) {
            return (this == obj);
        }
        ···

    Object 类提供的 equals 方法实际上是比较两个对象的哈希值

        // 这是 String 类提供的 equals()
        ···
        public boolean equals(Object anObject{
            if (this == anObject) {
                return true;
            }
            if (anObject instanceof String) {
                String anotherString = (String)anObject;
                int n = value.length;
                if (n == anotherString.value.length) {
                    char v1[] = value;
                    char v2[] = anotherString.value;
                    int i = 0;
                    while (n-- != 0) {
                        if (v1[i] != v2[i])
                            return false;
                        i++;
                    }
                    return true;
                }
            }
            return false;
        }
        ···

    String 类提供的 equals 方法也会比较哈希值,但并不仅仅之是比较哈希值

    如果两个对象的哈希值相同就说明它们包含的内容一定是相同的,直接返回 true,但如果哈希值不同且传参进来的对象非 String 类型则直接返回 false

    当两个对象均为 String 类型且长度一致时,则通过 while 循环逐个字符进行比对,并返回最终对比结果

        ···
        public String toString() {
            return getClass().getName() + "@" + Integer.toHexString(hashCode());
        }
        ··

    toString():获取对象的字符串表现形式,组合方式为:类名+@+十六进制哈希码。当然了,获取这样的数据实际意义不大,一般我们都是通过重写对象 toString() 来传递更多具体的数据,如:重写实体 Bean 的 toString() 观察数据是否正确或完整

        ···
        protected native Object clone() throws CloneNotSupportedException;
        ···

    clone():创建对象的副本并返回

    提供克隆功能的目标对象需实现 Cloneable 接口并重写 clone(),实现此功能遇到以下场景时会涉及两个概念,依需而选:

    如果此对象被复制的属性都是基本类型,那么只需要实现当前类的 Cloneable 机制就可以了,这种称之为浅拷贝。如果被复制对象的属性中包含其它实体类对象的引用,且这些实体类对象都需要实现cloneable接口并覆盖clone()方法,这种称之为深拷贝(其它实体类不实现 Cloneable 机制也可进行拷贝,但就是浅拷贝了,这时指针是指向此实体类原地址的,而非新建地址,因为它并未创建副本)

    浅拷贝:被复制对象的所有值属性都含有与原来对象的相同,而所有的对象引用属性仍然指向原来的对象
    深拷贝:在浅拷贝的基础上,所有引用其它对象的变量也进行了clone,并指向被复制过的新对象

        ···
        public final native void notify();
        ···

    notify():唤醒正在等待此对象的监视器

    线程成为此对象监视器的方法有三种:通过执行此对象的 Synchronized 方法、通过执行属于此对象的 Synchronized 代码块、通过执行该类的静态 Synchronized 方法,如果该线程不是锁的持有者,则会抛出 IllegalMonitorStateException 异常

    当唤醒发生时,如果有多个线程正在等待此对象,那么其中一个将会被唤醒,但选择是随机的(这取决于虚拟机中本功能的具体实现代码)

        ···
        public final native void notifyAll();
        ···

    notifyAll():唤醒正在等待此对象监视器的所有线程

    但此时这些等待线程不会立即执行,它们需要等待调用 notifyAll() 的线程释放掉锁后才会执行。同样的,如果该线程不是锁的持有者,调用 notifyAll() 会抛出 IllegalMonitorStateException 异常

        ···
        public final native void wait(long timeout) throws InterruptedException;

        ···
        public final void wait() throws InterruptedException {
            wait(0);
        }

        ···
        public final void wait(long timeout, int nanos) throws InterruptedException {
            if (timeout 0) {
                throw new IllegalArgumentException("timeout value is negative");
            }

            if (nanos 0 || nanos > 999999) {
                throw new IllegalArgumentException(
                                    "nanosecond timeout value out of range");
            }

            if (nanos > 0) {
                timeout++;
            }

            wait(timeout);
        }
        ···

    wait() / wait(long timeout) / wait(long timeout, int nanos):使调用该方法的线程释放共享资源锁,然后从运行状态退出,进入等待队列,直到被再次唤醒 或 定时等待 N 毫秒(如果没有通知就超时返回)

    使用时首先要获得锁,需在 synchronized 方法或 synchronized 代码块中调用,由 notify() 或 notifyAll() 唤醒

        ···
        protected void finalize() throws Throwable { }

    finalize():资源回收

    它会在gc启动,该对象被回收的时候调用


    常见问题

    final / finally() / finalize() 的区别

    final 修饰符,可修饰属性、方法、类。修饰属性时表示常量,只可被赋值一次,修饰方法时表示方法锁定,以防止继承类对其进行更改,修饰类表示常量类不可被继承(final类中所有的成员方法也都会隐式的定义为final方法)

    finally 异常处理,它只能用在try/catch语句中,并且附带一个语句块,表示这段语句最终一定会被执行(不管有没有抛出异常),经常被用在需要释放资源的情况下

    finalized() 资源回收,它会在gc启动,该对象被回收的时候调用,用于释放某些资源

    长按订阅更多精彩▼

    40b0d763d4ea018a312189a19838a691.png

    如有收获,点个在看,诚挚感谢7fddefb8f4ae1022286bfc8d0d88025a.png

    展开全文
  • HashMap、TreeMap 继承自 AbstractMap 类,Hashtable 继承自 Dictionary 类,三者均实现 Map 接口 HashMap 源码解析 HashMap() public HashMap(int initialCapacity, float loadFactor){ // ... this....

    4528474cb445574698e189e2023ff106.png

    作者:MobMsg,资深全端工程师一枚,架构师社区合伙人!


    忽如一夜春风来,千树万树梨花开。山回路转不见君,雪上空留马行处。若君点赞留香丨留言鼓励丨指出不足,此谊绵绵无绝期!


    HashMap 结构示意图

    3f7313782de0ba53ef2002984b410b0e.png

    HashMap 是数组和链表组合组成的复杂结构,哈希值决定了键值在数组的位置,当哈希值相同时则以链表形式存储,当链表长度到达设定的阈值则会对其进行树化,这样做是为了保证数据安全和数据相关操作的效率

    HashMap 性能表现取决于哈希码的有效性,所以 hashCode 和 equals 的基本约定规则尤为重要,如:equals 相等,hashCode 一定要相等;重写了 hashCode 也要重写 equals;hashCode 需要保持一致性,状态改变返回的哈希值仍然要一致;equals 的对称、反射、传递等特性


    HashMap 与 Hashtable、TreeMap 的区别

    HashMap:基于数组的非同步哈希表,支持 null 键或值,是键值对存取数据场景的首选
    Hashtable:基于数组的同步哈希表,不支持null键或值,因为同步导致性能影响,很少被使用
    TreeMap:基于红黑树提供顺序访问的 Map,比 HashMap 节省空间,但它的数据操作(查、增、删)时间复杂度均为:O(log(n)),这点与 HashMap 不同。支持空值,当键为空时且未实现 Comparator 接口,会出现 NullPointerException ,实现了 Comparator 接口并对 null 对象进行判断可实现正常存入

    HashMap、Hashtable、TreeMap 均以键值对形式存储或操作数据元素。HashMap、TreeMap 继承自 AbstractMap 类,Hashtable 继承自 Dictionary 类,三者均实现 Map 接口


    HashMap 源码解析

    HashMap()

    public HashMap(int initialCapacity, float loadFactor){  
        // ... 
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

    初始化 HashMap 时仅设置了一些初始值,但在开始处理数据时,如 .put() 方法内渐渐开始复杂起来

    HashMap.put()

        public V put(K key, V value{
            return putVal(hash(key), key, valuefalsetrue);
        }

        final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict
    {
            // 定义新tab数组及node对象
            Node[] tab; Node p; int n, i;// 如果原table是空的或者未存储任何元素则需要先初始化进行tab的初始化if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;// 当数组中对应位置为null时,将新元素放入数组中if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, valuenull);// 若对应位置不为空时处理哈希冲突else {
                Node e; K k;// 1 - 普通元素判断: 更新数组中对应位置数据if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    e = p;// 2 - 红黑树判断:当p为树的节点时,向树内插入节点else if (p instanceof TreeNode)
                    e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);// 3 - 链表判断:插入节点else {for (int binCount = 0; ; ++binCount) {// 找到尾结点并插入if ((e = p.next) == null) {
                            p.next = newNode(hash, key, valuenull);// 判断链表长度是否达到树化阈值,达到就对链表进行树化if (binCount >= TREEIFY_THRESHOLD - 1// -1 for 1st
                                treeifyBin(tab, hash);break;
                        }// 更新链表中对应位置数据if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))break;
                        p = e;
                    }
                }// 如果存在这个映射就覆盖if (e != null) { // existing mapping for key
                    V oldValue = e.value;// 判断是否允许覆盖,并且value是否为空if (!onlyIfAbsent || oldValue == null)
                        e.value = value;// 回调以允许LinkedHashMap后置操作
                    afterNodeAccess(e); return oldValue;
                }
            }// 更新修改次数
            ++modCount;// 检查数组是否需要进行扩容if (++size > threshold)
                resize();// 回调以允许LinkedHashMap后置操作
            afterNodeInsertion(evict);return null;
        }

    当 table 为 null,会通过 resize() 初始化,且 resize() 有两个作用,一是创建并初始化 table ,二是在 table 容量不满足需求时进行扩容:

            if (++size > threshold)
                resize();

    具体的键值对存储位置计算方法为:

            if ((p = tab[i = (n - 1) & hash]) == null)
                // 向数组赋值新元素
                tab[i] = newNode(hash, key, valuenull);
            else {
                Node e; K k;// 如果新插入的结点和table中p结点的hash值,key值相同的话if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    e = p;// 如果是红黑树结点的话,进行红黑树插入else if (p instanceof TreeNode)
                    e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);else {for (int binCount = 0; ; ++binCount) {// 代表这个单链表只有一个头部结点,则直接新建一个结点即可if ((e = p.next) == null) {
                            p.next = newNode(hash, key, valuenull);// 链表长度大于8时,将链表转红黑树if (binCount >= TREEIFY_THRESHOLD - 1// -1 for 1st
                                treeifyBin(tab, hash);break;
                        }if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))break;// 及时更新p
                        p = e;
                    }
                }// 如果存在这个映射就覆盖if (e != null) { // existing mapping for key
                    V oldValue = e.value;// 判断是否允许覆盖,并且value是否为空if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                    afterNodeAccess(e);     // 回调以允许LinkedHashMap后置操作return oldValue;
                }
            }

    留意 .put() 方法中的 hash 计算,它并不是 key 的 hashCode ,而是将 key 的 hashCode 高位数据移位到低位进行异或运算,这样一些计算出来的哈希值主要差异在高位时的数据,就不会因 HashMap 里哈希寻址时被忽略容量以上的高位,那么即可有效避免此类情况下的哈希碰撞

        static final int hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }

    HashMap.resize()

        final Node[] resize() {// 把当前底层数组赋值给oldTab,为数据迁移工作做准备
            Node[] oldTab = table;// 获取当前数组的大小,等于或小于0表示需要初始化数组,大于0表示需要扩容数组int oldCap = (oldTab == null) ? 0 : oldTab.length;// 获取扩容的阈值(容量*负载系数)int oldThr = threshold;// 定义并初始化新数组长度和目标阈值int newCap, newThr = 0;// 判断是初始化数组还是扩容,等于或小于0表示需要初始化数组,大于0表示需要扩容数组。若  if(oldCap > 0)=true 表示需扩容而非初始化if (oldCap > 0) {// 判断数组长度是否已经是最大,MAXIMUM_CAPACITY =(2^30)if (oldCap >= MAXIMUM_CAPACITY) {// 阈值设置为最大
                    threshold = Integer.MAX_VALUE;return oldTab;
                }else if ((newCap = oldCap <1)                      oldCap >= DEFAULT_INITIAL_CAPACITY)                // 目标阈值扩展2倍,数组长度扩展2倍
                    newThr = oldThr <1; // double threshold
            }// 表示需要初始化数组而不是扩容else if (oldThr > 0// 说明调用的是HashMap的有参构造函数,因为无参构造函数并没有对threshold进行初始化
                newCap = oldThr;// 表示需要初始化数组而不是扩容,零初始阈值表示使用默认值else {    // 说明调用的是HashMap的无参构造函数
                newCap = DEFAULT_INITIAL_CAPACITY;// 计算目标阈值
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            }// 当目标阈值为0时需重新计算,公式:容量(newCap)*负载系数(loadFactor)if (newThr == 0) {float ft = (float)newCap * loadFactor;
                newThr = (newCap float)MAXIMUM_CAPACITY ?
                          (int)ft : Integer.MAX_VALUE);
            }// 根据以上计算结果将阈值更新
            threshold = newThr;// 将新数组赋值给底层数组@SuppressWarnings({"rawtypes","unchecked"})
                Node[] newTab = (Node[])new Node[newCap];
            table = newTab;// -------------------------------------------------------------------------------------// 此时已完成初始化数组或扩容数组,但原数组内的数据并未迁移至新数组(扩容后的数组),之后的代码则是完成原数组向新数组的数据迁移过程// -------------------------------------------------------------------------------------// 判断原数组内是否有存储数据,有的话开始迁移数据if (oldTab != null) {// 开始循环迁移数据for (int j = 0; j                 Node e;// 将数组内此下标中的数据赋值给Node类型的变量e,并判断非空if ((e = oldTab[j]) != null) {
                        oldTab[j] = null;// 1 - 普通元素判断:判断数组内此下标中是否只存储了一个元素,是的话表示这是一个普通元素,并开始转移if (e.next == null)
                            newTab[e.hash & (newCap - 1)] = e;// 2 - 红黑树判断:判断此下标内是否是一颗红黑树,是的话进行数据迁移else if (e instanceof TreeNode)
                            ((TreeNode)e).split(this, newTab, j, oldCap);// 3 -  链表判断:若此下标内包含的数据既不是普通元素又不是红黑树,则它只能是一个链表,进行数据转移else { // preserve order
                            Node loHead = null, loTail = null;
                            Node hiHead = null, hiTail = null;
                            Node next;do {
                                next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null)
                                        loHead = e;else
                                        loTail.next = e;
                                    loTail = e;
                                }else {if (hiTail == null)
                                        hiHead = e;else
                                        hiTail.next = e;
                                    hiTail = e;
                                }
                            } while ((e = next) != null);if (loTail != null) {
                                loTail.next = null;
                                newTab[j] = loHead;
                            }if (hiTail != null) {
                                hiTail.next = null;
                                newTab[j + oldCap] = hiHead;
                            }
                        }
                    }
                }
            }// 返回初始化完成或扩容完成的新数组return newTab;
        }

    容量和负载系数决定了数组容量,空余太多会造成空间浪费,使用太满会影响操作性能

    如果能够明确知道 HashMap 将要存取的键值对的数量,可以考虑预先设置合适的容量大小。具体数值我们可以根据扩容发生的条件来做简单预估,根据前面的代码分析,我们知道它需要符合计算条件:负载因子 * 容量 > 元素数量

    所以,预先设置的容量需要满足,大于 预估元素数量 / 负载因子,同时它是 2 的幂数

    但需要注意的是:
    如果没有特别需求,不要轻易进行更改,因为 JDK 自身的默认负载因子是非常符合通用场景的需求的。如果确实需要调整,建议不要设置超过 0.75 的数值,因为会显著增加冲突,降低 HashMap 的性能。如果使用太小的负载因子,按照上面的公式,预设容量值也进行调整,否则可能会导致更加频繁的扩容,增加无谓的开销,本身访问性能也会受影响。

    HashMap.get()

        public V get(Object key{
            Node e;return (e = getNode(hash(key), key)) == null ? null : e.value;
        }final Node getNode(int hash, Object key{
            Node[] tab; Node first, e; int n; K k;// 将table赋值给变量tab并判断非空 && tab 的厂部大于0 && 通过位运算得到求模结果确定链表的首节点赋值并判断非空if ((tab = table) != null && (n = tab.length) > 0 &&
                (first = tab[(n - 1) & hash]) != null) {// 判断首节点hash值 && 判断key的hash值(地址相同 || equals相等)均为true则表示first即为目标节点直接返回if (first.hash == hash && // always check first node
                    ((k = first.key) == key || (key != null && key.equals(k))))return first;// 若首节点非目标节点,且还有后续节点时,则继续向后寻找if ((e = first.next) != null) {// 1 - 树:判断此节点是否为树的节点,是的话遍历树结构查找节点,查找结果可能为nullif (first instanceof TreeNode)return ((TreeNode)first).getTreeNode(hash, key);// 2 - 链表:若此节点非树节点,说明它是链表,遍历链表查找节点,查找结果可能为nulldo {if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))return e;
                    } while ((e = e.next) != null);
                }
            }return null;
        }

    HashMap 为什么会被树化

    为了保证数据安全及相关操作效率

    因为在元素放置过程中,如果一个对象哈希冲突,都被放置到同一个桶里,则会形成一个链表,我们知道链表查询是线性的,会严重影响存取的性能

    而在现实世界,构造哈希冲突的数据并不是非常复杂的事情,恶意代码就可以利用这些数据大量与服务器端交互,导致服务器端 CPU 大量占用,这就构成了哈希碰撞拒绝服务攻击,国内一线互联网公司就发生过类似攻击事件


    本篇将持续更新 HashMap 相关知识,一起查漏补缺学个痛快!欢迎点赞留香丨留言鼓励丨指出不足!


    长按订阅更多精彩▼

    bfa6a0b638957c80552ca8020362cb64.png

    128333628910ad812e6a3a728c6d217e.gif

    展开全文
  • JVM 允许应用程序同时运行、执行多个线程,每个线程都有优先权,具有较高优先级的线程优先于优先级较低的线程执行在Java中线程分为两类:User Thread(用户线程)、Daemon Thread(守护线程)在JVM启动时候会调用ma...

    441f46d542db2947de24fa4d31f26963.png

    作者:MobMsg,资深全端工程师一枚,架构师社区合伙人!

    Thread 相关概念

    线程是系统资源分配的最小单位,它被包含在进程之中,是进程中的实际运作单位。JVM 允许应用程序同时运行、执行多个线程,每个线程都有优先权,具有较高优先级的线程优先于优先级较低的线程执行

    在Java中线程分为两类:User Thread(用户线程)Daemon Thread(守护线程)

    在JVM启动时候会调用main函数,main函数所在的线程是就是一个用户线程,在此线程中新建的线程默认都是用户线程,但通过 Thread.setDaemon(true) 可设置守护线程(需在 Thread.start() 前调用)。守护线程是JVM中所有非守护线程的保姆,守护线程最典型的应用就是 GC (垃圾回收器)

    只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作,当JVM中的最后一个用户线程结束时,守护线程随着JVM一同结束


    Thread 创建方式

    创建线程的方式有两种:继承 Thread 类实现 Runnable 接口

    Thread 类本身也是通过实现 Runnable 接口来完成创建的。创建线程时可以为线程指定名称,名称可重复。如果在创建线程时未指定名称,则会为其生成新名称

    继承 Thread 类创建与启动:

         class PrimeThread extends Thread {
             long minPrime;
             PrimeThread(long minPrime) {
                 this.minPrime = minPrime;
             }

             public void run() {
                 // compute primes larger than minPrime
             }
         }
         PrimeThread p = new PrimeThread(143);
         p.start();

    实现 Runnable 接口创建与启动:

         class PrimeRun implements Runnable {
             long minPrime;
             PrimeRun(long minPrime) {
                 this.minPrime = minPrime;
             }

             public void run() {
                 // compute primes larger than minPrime
             }
         }
         PrimeRun p = new PrimeRun(143);
         new Thread(p).start();

    Thread 状态变化

    线程的状态可在 Thread 类源码中找到,共 6 个:NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATED

        public enum State {

            NEW, // 尚未启动状态

            RUNNABLE, // 可运行状态,但它可能还在等待处理器资源分配

            BLOCKED, // 阻塞状态

            WAITING, // 等待状态,等待另一个线程执行完毕

            TIMED_WAITING, // 定时等待状态

            TERMINATED; // 终止状态,线程已执行完毕
        }
    1620d3f670d7d39bd511b9c58ba17308.png

    Thread 类源码中的关键方法

    Thread.start():启动线程

    调用 start() 时,会首先检查是否是首次启动此线程,也就是threadStatus == 0,如果 threadStatus != 0 说明当前为重复启动,这是不允许的,会抛出线程状态异常错误 IllegalThreadStateException

    通过校验后,会将当前线程的实例对象加入线程组 group ,之后通过本地方法 start0() 执行启动线程的操作。启动完成后在threadStartFailed()内执行移除操作 ,释放资源

        public synchronized void start({

            if (threadStatus != 0)
                throw new IllegalThreadStateException();

            group.add(this);

            boolean started = false;
            try {
                start0();
                started = true;
            } finally {
                try {
                    if (!started) {
                        group.threadStartFailed(this);
                    }
                } catch (Throwable ignore) {
                    ···
                }
            }
        }

        private native void start0();

    Thread.stop():强制停止线程执行

    这个方法是不安全的,很可能会产生不可预料的结果,就好比通过断电源关机,而不是通过正常关机操作来完成,结果相同,过程却完全不同

    调用 stop() 将会抛出一个ThreadDeath异常,这时候run方法也就执行结束了,线程就终止了,这种是用抛异常来结束线程的

    结束后会释放子线程所持有的所有锁,一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误

        @Deprecated
        public final void stop() {
            SecurityManager security = System.getSecurityManager();
            if (security != null) {
                checkAccess();
                if (this != Thread.currentThread()) {
                    security.checkPermission(SecurityConstants.STOP_THREAD_PERMISSION);
                }
            }

            if (threadStatus != 0) {
                resume(); // Wake up thread if it was suspended; no-op otherwise
            }

            stop0(new ThreadDeath());
        }

        @Deprecated
        public final synchronized void stop(Throwable obj) {
            throw new UnsupportedOperationException();
        }

    Thread.run():线程实际运行的代码块

    需要注意的是,如果要启动一个线程,直接调用 run() 方法是无效的,它不会产生任何实际结果

        @Override
        public void run() {
            if (target != null) {
                target.run();
            }
        }

    Thread.interrupt():修改中断状态,以实现合理、安全的中断当前线程

    stop() 方法也可中断线程,但它是立即终止,会引发一些未知的问题,所以就出现了 interrupt() 方法,用它可实现有条件的终止线程,使得数据安全得到保障

    要想真正实现线程中断, interrupt() 需要配合 isInterrupted()interrupted() 一起使用,这两个方法可以获取中断标记是否为 true,获取后我们就可以做合理的处理。调用 isInterrupted() 会返回中断状态但不会还原状态, interrupted() 会返回中断状态并清除中断状态,根据实际业务需求分别使用即可

        public void interrupt() {
            if (this != Thread.currentThread())
                checkAccess();

            synchronized (blockerLock) {
                Interruptible b = blocker;
                if (b != null) {
                    interrupt0();           // Just to set the interrupt flag
                    b.interrupt(this);
                    return;
                }
            }
            interrupt0();
        }
        public static boolean interrupted() {
            return currentThread().isInterrupted(true);
        }
        public boolean isInterrupted() {
            return isInterrupted(false);
        }

    Thread.yield():暂停执行当前线程,让出cpu执行其它线程(但是可能会被忽略)

    实际上,如果想让 yield()发挥它的作用,需要搭配线程 优先级 来使用,它的实际运行流程是先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程。

        public static native void yield();

    Thread.wait() / Thread.wait(long):使调用该方法的线程释放共享资源锁,然后从运行状态退出,进入等待队列,直到被再次唤醒 或 定时等待 N 毫秒,如果没有通知就超时返回

    wait() 属于 Object 对象。使用时首先需要获得锁,一般放在同步方法或同步代码块中( synchronized),由 notify()notifyAll() 唤醒

        public final void wait() throws InterruptedException {
            wait(0);
        }

        public final native void wait(long timeout) throws InterruptedException;

    Thread.join() / Thread.join(long):等待该线程结束后,再继续

    其作用就是将调用join的线程优先执行,当前正在执行的线程阻塞,直到调用join方法的线程执行完毕或者被打断,主要用于线程之间的交互

     public final void join() throws InterruptedException {
            join(0);
        }

        public final synchronized void join(long millis)throws InterruptedException {
            long base = System.currentTimeMillis();
            long now = 0;

            if (millis 0) {
                throw new IllegalArgumentException("timeout value is negative");
            }

            if (millis == 0) {
                while (isAlive()) {
                    wait(0);
                }
            } else {
                while (isAlive()) {
                    long delay = millis - now;
                    if (delay <= 0) {
                        break;
                    }
                    wait(delay);
                    now = System.currentTimeMillis() - base;
                }
            }
        }

    Thread.sleep(long):使当前线程在指定的时间内暂停执行

    暂停过程中调用此线程对象的 interrupt() 会唤醒线程并抛出InterruptedException,之后继续执行

        public static void sleep(long millis, int nanos)throws InterruptedException {
            if (millis 0) {
                throw new IllegalArgumentException("timeout value is negative");
            }

            if (nanos 0 || nanos > 999999) {
                throw new IllegalArgumentException(
                                    "nanosecond timeout value out of range");
            }

            if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
                millis++;
            }

            sleep(millis);
        }

    Thread.notify() / Thread.notifyAll():唤醒正在等待状态的线程

    常见使用场景是一个线程A调用了对象B的wait()方法进入等待状态,而另一个线程C调用了对象B的notify()/notifyAll()方法,线程A收到通知后退出等待队列,进入可运行状态,进而执行后续操作。A 和 C 两个线程通过对象B来完成交互,而对象上的wait()方法和notify()/notifyAll()方法的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

    notify() 随机唤醒等待队列中等待同一共享资源的线程,此线程回退出等待队列,进入可运行状态

    notifyAll() 唤醒所有正在等待队列中等待同一共享资源的全部线程,全部退出等待队列,进入可运行状态,优先级最高的开始执行

       public final native void notify();

       public final native void notifyAll();

    Thread 类源码中的其它方法

    currentThread():返回当前正在执行的线程对象的引用

        /**
         * Returns a reference to the currently executing thread object.
         *
         * @return  the currently executing thread.
         */

        public static native Thread currentThread();

    这是一个 Native 方法,明这个方法是原生函数,是用C/C++语言实现的,并且被编译成了DLL,由java去调用,函数的实现体在DLL中,JDK的源代码中并不包含,所以我们看不到。这也是java的底层机制,实际上java就是在不同的平台上调用不同的native方法实现对操作系统的访问的。

        System.out.println("对象信息:" + Thread.currentThread());

        //// 输出结果
        对象信息:Thread[main,5,main]

    getId():返回该线程的ID

        /**
         * Returns the identifier of this Thread.  The thread ID is a positive
         * long number generated when this thread was created.
         * The thread ID is unique and remains unchanged during its lifetime.
         * When a thread is terminated, this thread ID may be reused.
         *
         * @return this thread's ID.
         * @since 1.5
         */

        public long getId() {
            return tid;
        }

        // tid 在 Thread 的 init 中赋值
        ···
        tid = nextThreadID();
        ···

        // tid 等于 threadSeqNumber,而 threadSeqNumber 专门被用来生成线程的 ID
        private static synchronized long nextThreadID() {
            return ++threadSeqNumber;
        }

    线程的ID是long类型,由nextThreadID方法生成,nextThreadID方法是线程安全的(synchronized 修饰),每次新建线程ID就++并赋值给tid

        System.out.println("线程  ID:" + Thread.currentThread().getId());

        //// 输出结果(main方法中调用)
        线程  ID:1

    getName() :获取线程的名称

        /**
         * Returns this thread's name.
         *
         * @return  this thread's name.
         * @see     #setName(String)
         */

        public final String getName() {
            return name;
        }

    线程名称是String类型,默认为 Thread-N (N:线程创建的顺序,从0开始)。当然了,Thread类也提供了2种修改名称的方法,即:new Thread("name") 或 Thread.setName("name")

    Thread threadTest01 = new Thread();
    System.out.println("线程名称:" + threadTest01.getName());
    Thread threadTest02 = new Thread();
    System.out.println("线程名称:" + threadTest02.getName());
    Thread threadTest03 = new Thread("我有名字,我叫 T03");
    System.out.println("线程名称:" + threadTest03.getName());
    Thread threadTest04 = new Thread();
    threadTest04.setName("我有名字:我叫 T04");
    System.out.println("线程名称:" + threadTest04.getName());

        //// 输出结果
        线程名称:Thread-0
        线程名称:Thread-1
        线程名称:我有名字,我叫 T03
        线程名称:我有名字:我叫 T04

    getPriority():获取线程优先级

        /**
         * The minimum priority that a thread can have.
         */

        public final static int MIN_PRIORITY = 1;

       /**
         * The default priority that is assigned to a thread.
         */

        public final static int NORM_PRIORITY = 5;

        /**
         * The maximum priority that a thread can have.
         */

        public final static int MAX_PRIORITY = 10;

        // 线程优先级在初始化(init)时设置,默认是等同于父线程优先级
        private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc,boolean inheritThreadLocals) {
            ...
            Thread parent = currentThread();
            ···
            this.priority = parent.getPriority();
            ···
        }

        /**
         * Returns this thread's priority.
         *
         * @return  this thread's priority.
         * @see     #setPriority
         */

        public final int getPriority() {
            return priority;
        }

    线程优先级默认为父线程的优先级 :this.priority = parent.getPriority(); 。线程的优先级不能决定线程的执行次序,但较高的优先级获取CPU资源的概率较大

    线程的优先级可通过 setPriority(int newPriority) 设置,参数的取值范围为 1 - 10,默认为 5

        public final void setPriority(int newPriority) {
            ThreadGroup g;
            checkAccess();
            if (newPriority > MAX_PRIORITY || newPriority             throw new IllegalArgumentException();
            }
            if((g = getThreadGroup()) != null) {
                if (newPriority > g.getMaxPriority()) {
                    newPriority = g.getMaxPriority();
                }
                setPriority0(priority = newPriority);
            }
        }

    需要注意的是,设置线程优先级时,不能大于最大优先级,否则会发生throw new IllegalArgumentException();,也不能大于所在线程组的最高优先级,否则将被重置为线程组的优先级newPriority = g.getMaxPriority();,如下:

    public static void main(String[] args) {
    ThreadMethods main = new ThreadMethods();
    Thread t01 = main.new MyThread01();
    Thread t02 = main.new MyThread02();
    t01.setPriority(Thread.MAX_PRIORITY);
    t02.setPriority(Thread.MIN_PRIORITY);
    t02.start();
    t01.start();
    }

    /**
     * @des 测试线程 01
     * */

    class MyThread01 extends Thread{
    public void run() {
    super.run();
    for(int i = 0 ; i 10 ; i++ ){
    System.out.println("MyThread01:" + i);
    try {
    Thread.sleep(500);
    catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
    }

    /**
     * @des 测试线程 02
     * */

    class MyThread02 extends Thread{
    public void run() {
    super.run();
    for(int i = 0 ; i 10 ; i++ ){
    System.out.println("MyThread02:" + i);
    try {
    Thread.sleep(500);
    catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
    }

     输出结果
    MyThread01:0
    MyThread02:0
    MyThread02:1
    MyThread01:1
    MyThread02:2
    MyThread01:2
    MyThread02:3
    MyThread01:3
    MyThread01:4
    MyThread02:4
    MyThread01:5
    MyThread02:5
    MyThread01:6
    MyThread02:6
    MyThread01:7
    MyThread02:7
    MyThread01:8
    MyThread02:8
    MyThread01:9
    MyThread02:9

    Thread 使用中常见问题

    死锁产生的原因以及如何避免和解决

    死锁产生的原因是多个线程同时被阻塞的情况下,它们中的一个或全部都在等待某个资源被释放,由于线程被无限阻塞,其余线程不可能等到此资源被释放,因此程序不能再继续正常运行(举个生动的栗子:一个装载有价值连城宝藏的宝箱需要两把钥匙才能打开,有两个人各有一把钥匙,但是互相都在等待对方先交出钥匙,但他们谁都不交出自己的钥匙并就此一直僵持下去)

    为什么产生死锁?

    产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生

    • 互斥条件:线程要求对所分配的资源进行排他性控制,即在一段时间内某 资源仅为一个进程所占有.此时若有其他进程请求该资源.则请求进程只能等待.

    • 不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的线程自己来释放(只能是主动释放).

    • 请求和保持条件:线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有,此时请求线程被阻塞,但对自己已获得的资源保持不放.

    • 循环等待条件:存在一种线程资源的循环等待链,链中每一个线程已获得的资源同时被链中下一个线程所请求。

    死锁产生条件总结:

    1. 有至少一个资源不能共享

    2. 至少有一个任务必须持有一个资源并且等待获取另一个被别的任务持有的资源

    3. 资源不能任务抢占

    4. 必须有循环等待

    如何避免和解决死锁?

    1、避免嵌套锁
    这是死锁最常见的原因,如果您已经持有一个资源,请避免锁定另一个资源。如果只使用一个对象锁,则几乎不可能出现死锁情况,比如以下代码对上边的循环嵌套部分进行修改,则避免了死锁的情况:

    public void run({
            String name = Thread.currentThread().getName();
            System.out.println(name + " acquiring lock on " + obj1);
            synchronized (obj1) {
                System.out.println(name + " acquired lock on " + obj1);
                work();
            }
            System.out.println(name + " released lock on " + obj1);
            System.out.println(name + " acquiring lock on " + obj2);
            synchronized (obj2) {
                System.out.println(name + " acquired lock on " + obj2);
                work();
            }
            System.out.println(name + " released lock on " + obj2);

            System.out.println(name + " finished execution.");
        }

    2、只锁需要的部分
    只获对需要的资源加锁,例如在上面的程序中,我们锁定了完整的对象资源,但是如果我们只需要其中一个字段,那么我们应该只锁定那个特定的字段而不是完整的对象

    3、避免无限期等待

    如果两个线程使用 thread join 无限期互相等待也会造成死锁,我们可以设定等待的最大时间来避免这种情况。

    JAVA 中锁的种类与区别

    在代码执行过程中,一些数据需要进行排他性的控制以保证最终计算结果的正确性,所以需要有一种机制保证在执行过程中此数据被锁住不会被外界修改,这种机制就是锁机制

    同时,根据锁的特性、设计、状态不同,又可以不严格的分为以下几类:

    公平锁/非公平锁

    公平锁指多个线程按照申请锁的顺序来获取锁,非公平锁就是没有顺序完全随机,可能会造成优先级反转或饥饿显现

    Synchronized 是非公平锁,ReentrantLock 通过构造函数可以决定是公平锁还是非公平锁,模式是非公平锁

    非公平锁的吞吐量性能比公平锁要更大

    可重入锁

    也叫递归锁,指同一线程在外层方法获取锁的时候,进入内层方法会自动获取锁

    Synchronized 和 ReentranLock 都是可重入锁,可在一定程度上避免死锁

    独享锁/共享锁

    独享锁是指该锁一次只能被一个县城持有,共享锁是指该锁可以被多个想成持有

    Synchronized 和 ReentranLock 都是独享锁。ReadWriteLock 的读锁是共享锁,写锁是独占锁。ReentrantLock 的独享锁和共享锁也是通过 AQS 来实现的

    互斥锁/读写锁

    互斥锁 = 独享锁,读写锁 = 共享锁。互斥锁实质就是 ReentrantLock,读写锁实质就是 ReadWriteLock

    乐观锁/悲观锁

    它们不属于具体的锁分类,而是看待并发同步的角度

    乐观锁认为对于同一数据的并发操作是不会发生修改的,在更新数据的时候回采用不断的尝试更新,乐观锁认为不加锁的并发操作是没事的

    悲观锁认为对于同一个数据的并发操作一定是会发生修改的,因此对于统一数据的并发操作,悲观锁采取加锁形式,因为悲观锁认为不加锁的操作一定会有问题

    悲观锁适合操作非常多的场景,乐观锁适合读写非常多的场景,不加锁可以大大提高性能

    分段锁

    其实是一种锁的策略,不是具体锁。如 ConcurrentHashMap 并发的实现就是通过分段锁的形式来实现高效并发操作

    当要 put 元素时并不是对整个 hashMap 加锁,而是先通过 hashCode 知道它要放在哪个分段,然后对分段进行加锁,所以多线程 put 元素时只要放在不同分段就是做到真正的并行插入,但是统计 size 时就需要获取所有的分段锁才能计算

    分段锁的设计是为了细化锁的粒度

    偏向锁/轻量级锁/重量级锁

    这是按照锁状态来归纳的,并且是针对 Synchronized 的。java 1.6 为了减少获取锁是释放锁带来的性能问题引入了一种状态,它会随着竞争情况逐渐升级,锁可以升级但不可降级,意味着偏向锁升级成轻量级锁后无法回撤,这种升级无法降级的策略目的就是为了提高活的锁和释放锁的效率

    自旋锁

    其实是相对于互斥锁的概念,互斥锁线程会进入 WAITING 状态和 RUNNABLE 状态的奇幻,涉及上下文切换、CPU抢占等开销,自旋锁的线程一直是 RUNNABLE 状态,一直在那循环检测锁标志位,机制不重复,但是自旋锁加锁全程消耗 CPU,起始开销虽然低于互斥锁,但随着持锁时间加锁开销是线性增长

    可中断锁

    Synchronized 是不可中断的,Lock 是可中断的

    这里的可中断建立在阻塞等待中断,运行中是无法中断的

    长按订阅更多精彩▼

    b2a7827bc0bb806d3597e96636d33f6e.png

    如有收获,点个在看,诚挚感谢aa4330b0ae0232d2cccc3f4c07b10d57.png

    展开全文
  • java源码书籍推荐

    千次阅读 2018-08-13 09:20:08
    有没有一些java源码书籍推荐,最好是同时提供分析源码思路的书籍
  • java源码包---java 源码 大量 实例

    千次下载 热门讨论 2013-04-18 23:15:26
    Applet钢琴模拟程序java源码 2个目标文件,提供基本的音乐编辑功能。编辑音乐软件的朋友,这款实例会对你有所帮助。 Calendar万年历 1个目标文件 EJB 模拟银行ATM流程及操作源代码 6个目标文件,EJB来模拟银行ATM...
  • 基础知识讲解,并配有附加代码,详细分析以及图解,比市面上书籍专业很多,很适合初学者读阅。读完,把代码码一遍,java包会哦。
  • Java 集合框架(Java Collections Framework, JCF)包含很多平时开发中的常用类,例如 List、Set、ArrayList、HashMap、HashSet 等,因此打算先从这里下手。而 Collection 接口又是集合层次中的根接口,最常用的 ...
  • 在 JDK 1.5 之前,Java 通过 synchronized 关键字来实现锁的功能,该方式是语法层面的,由 JVM 实现。JDK 1.5 增加了锁在 API 层面的实现,也就是 java.util.concurrent.locks.Lock 接口及其相关的实现类,它不仅...
  • 还记得大学快毕业的时候要准备找工作了,然后就看各种面试相关的书籍,还记得很多面试书中都说到: HashMap是非线程安全的,HashTable是线程安全的。 那个时候没怎么写Java代码,所以根本就没有听说过...
  • 适用人群:具有Java基础的人群,希望学习Java多线程、并发编程技术的人群。课程概述:阻塞队列 (BlockingQueue)是Java util.concurrent包下重要的数据结构,BlockingQueue提供了线程安全的队列访问方式,当阻塞队列...
  • Java知识总结 该项目主要分享一些个人经验,以及一些个人项目中遇到的问题;还有就是一些读书笔记。 如果大家觉得该项目还不错,可以帮忙star或者fork下,你的star就是我的动力,谢谢! 为开源贡献自己的一份力量。 ...
  • java源码包2

    千次下载 热门讨论 2013-04-20 11:28:17
    Applet钢琴模拟程序java源码 2个目标文件,提供基本的音乐编辑功能。编辑音乐软件的朋友,这款实例会对你有所帮助。 Calendar万年历 1个目标文件 EJB 模拟银行ATM流程及操作源代码 6个目标文件,EJB来模拟银行...
  • java源码包3

    千次下载 热门讨论 2013-04-20 11:30:13
    Applet钢琴模拟程序java源码 2个目标文件,提供基本的音乐编辑功能。编辑音乐软件的朋友,这款实例会对你有所帮助。 Calendar万年历 1个目标文件 EJB 模拟银行ATM流程及操作源代码 6个目标文件,EJB来模拟银行...
  • java源码包4

    千次下载 热门讨论 2013-04-20 11:31:44
    Applet钢琴模拟程序java源码 2个目标文件,提供基本的音乐编辑功能。编辑音乐软件的朋友,这款实例会对你有所帮助。 Calendar万年历 1个目标文件 EJB 模拟银行ATM流程及操作源代码 6个目标文件,EJB来模拟银行...
  • 所以趁着找实习的准备,结合以前的学习储备,创建一个主要针对应届生和初学者的 Java 开源知识项目,专注 Java 后端面试题 + 解析 + 重点知识详解 + 精选文章的开源项目,希望它能伴随你我一直进步! 说明:此项目...
  • 所以趁着找实习的准备,结合以前的学习储备,创建一个主要针对应届生和初学者的 Java 开源知识项目,专注 Java 后端面试题 + 解析 + 重点知识详解 + 精选文章的开源项目,希望它能伴随你我一直进步! 说明:此项目...
  • 市场上技术书籍很少介绍的void,其实void是一种基本的数据类型。位于java.lang.Void, 源码内容不多,附上源码比较好理解些。package java.lang;/** * The {@code Void} class is an uninstantiable placeholder ...
  • 所以趁着找实习的准备,结合以前的学习储备,创建一个主要针对应届生和初学者的 Java 开源知识项目,专注 Java 后端面试题 + 解析 + 重点知识详解 + 精选文章的开源项目,希望它能伴随你我一直进步! 说明:此项目...
  • 这里,我们重点关注二叉排序树,所以只会介绍一些必需了解的概念,关于树的更多知识,大家可以查看相关书籍进行系统的学习。 树的定义 树(Tree)是n(n≥0) 个结点的有限集。n=0 时称为空树。在任意一棵非空树中: 1....
  • 不知道有什么书,或哪里可以找到相关的资料
  • 原来在学JAVA时,那些JAVA入门书籍会告诉你一些规律还有法则,但是用的时候我们一般很难想起来,因为我们用的少并且不知道为什么。知其所以然方能印象深刻并学以致用。 本篇文章针对JAVA中的MMAP的文件映射读写机制...
  • 基础知识讲解,并配有附加代码,详细分析以及图解,比市面上书籍专业很多,很适合初学者读阅。读完,把代码码一遍,java包会哦。
  • Applet钢琴模拟程序java源码 2个目标文件,提供基本的音乐编辑功能。编辑音乐软件的朋友,这款实例会对你有所帮助。 Calendar万年历 1个目标文件 EJB 模拟银行ATM流程及操作源代码 6个目标文件,EJB来模拟银行ATM机...
  • Applet钢琴模拟程序java源码 2个目标文件,提供基本的音乐编辑功能。编辑音乐软件的朋友,这款实例会对你有所帮助。 Calendar万年历 1个目标文件 EJB 模拟银行ATM流程及操作源代码 6个目标文件,EJB来模拟银行ATM机...
  • Hibernate源码分析

    2015-01-11 22:49:53
    javahibernateorm框架源码分析  先扯二句蛋:做Java也有很久了,安卓也搞了半年,回想自己当初学习java,j2ee的时候,全靠自己,没有老师,没有同学,书籍就是老师,搜索引擎就是同学,磕磕绊绊下来,终于有所...

空空如也

空空如也

1 2 3 4 5 ... 14
收藏数 263
精华内容 105
关键字:

java源码分析书籍

java 订阅