开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

多叉树结合JavaScript树形控件实现无限级树形结构(一

发表于 2021-06-05

一、问题研究的背景和意义

在Web应用程序开发领域,基于Ajax技术的JavaScript树形控件已经被广泛使用,它用来在Html页面上展现具有层次结构的数据项。

目前市场上常见的JavaScript框架及组件库中均包含自己的树形控件,例如jQuery、Ext JS等,还有一些独立的树形控件,例如dhtmlxTree等,这些树形控件完美的解决了层次数据的展示问题。展示离不开数据,树形控件主要利用Ajax技术从服务器端获取数据源,数据源的格式主要包括JSON、XML等,而这些层次数据一般都存储在数据库中。

“无限级树形结构”,顾名思义,没有级别的限制,它的数据通常来自数据库中的无限级层次数据,这种数据的存储表通常包括id和parentId这两个字段,以此来表示数据之间的层次关系。

现在问题来了,既然树形控件的数据源采用JSON或XML等格式的字符串来组织层次数据,而层次数据又存储在数据库的表中,那么如何建立起树形控件与层次数据之间的关系,换句话说,如何将数据库中的层次数据转换成对应的层次结构的JSON或XML格式的字符串,返回给客户端的JavaScript树形控件?这就是我们要解决的关键技术问题。

本文将以目前市场上比较知名的Ext JS框架为例,讲述实现无限级树形结构的方法,该方法同样适用于其它类似的JavaScript树形控件。

Ext JS框架是富客户端开发中出类拔萃的框架之一。在Ext的UI控件中,树形控件无疑是最为常用的控件之一,它用来实现树形结构的视图。TreeNode用来实现静态的树形结构,AsyncTreeNode用来实现动态的异步加载树形结构,后者最为常用,它通过接收服务器端返回来的JSON格式的数据,动态生成树形结构节点。

动态生成树有两种思路:

  • 一种是一次性生成全部树节点;
  • 另一种是逐级加载树节点(利用Ajax,每次点击节点时查询下一级节点)。

对于大数据量的树节点来说,逐级加载是比较合适的选择,但是对于小数据量的树节点来说,一次性生成全部节点应该是最为合理的方案。在实际应用开发中,一般不会遇到特别大数据量的场景,所以一次性生成全部树节点是我们重点研究的技术点,也就是本文要解决的关键技术问题。

本文以基于Ext JS的应用系统为例,讲述如何将数据库中的无限级层次数据一次性在界面中生成全部树节点(例如在界面中以树形方式一次性展示出银行所有分支机构的信息),同时对每一个层次的节点按照某一属性和规则排序,展示出有序的树形结构。

解决一次性构造无限级树形结构的问题,可以拓展出更多的应用场景,例如树形结构表格TreeGrid,一次性生成树形表格,对树形表格进行完整分页,对表格列进行全排序;或者可以利用本文的思路扩展出其他的更复杂的应用场景。

先看两个图例,有个直观上的认识:

图一,银行分支机构树形结构

廊坊树菜单.jpg

图二,树形结构表格

中国.jpg

二、详细设计方案

让我们先看两段代码片段:

文件一,branchTree.html (Ext树形控件页面)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
js复制代码Ext.onReady(
function(){
var tree = new Ext.tree.TreePanel({
height: 300,
width: 400,
animate:true,
enableDD:true,
containerScroll: true,
rootVisible: false,
frame: true,
// getBranch.do请求服务器返回多级树形结构的JSON字符串
loader: new Ext.tree.TreeLoader({dataUrl:'getBranch.do'}),
root : new Ext.tree.AsyncTreeNode({id:'0',text:'根结点'})
});
tree.expandAll();
}
);

文件二,branchTreeJSON.jsp (接收getBranch.do请求,返回多级树形结构的JSON字符串)

1
2
3
4
5
6
7
8
9
java复制代码<%
// 读取银行分支机构的层次数据
List result = DataAccess.getBankInfoList();
// 将层次数据转换为多叉树对象(本文下面会详细介绍该数据结构的实现方法)
Node root = ExtTreeHelper.createExtTree(result);
%>
[
<%=root.toString()%> <!-- 以JSON的形式返回响应数据,Ext.tree.TreeLoader会根据此数据生成树形结构 -->
]

以上两个程序文件是一次性生成无限级树形结构所必须的,其中最为关键的部分就是如何生成一个无限级的树形结构JSON字符串,返回给客户端的Ext树形控件。对于银行分支机构来说,需要返回类似如下的JSON串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
js复制代码{
id: '100000',
text: '廊坊银行总行',
children: [
{
id: '110000',
text: '廊坊分行',
children: [
{
id: '113000',
text: '廊坊银行开发区支行',
leaf: true
},
{
id: '112000',
text: '廊坊银行解放道支行',
children: [
{
id: '112200',
text: '廊坊银行三大街支行',
leaf: true
},
{
id: '112100',
text: '廊坊银行广阳道支行',
leaf: true
}
]
},
{
id: '111000',
text: '廊坊银行金光道支行',
leaf: true
}
]
}
]
}

同时还需要对树中每一个层次的节点按照某一属性(比如分支机构编号)进行排序,以展示出有序的树形结构。

现在可以把问题概括为:

1、把数据库中的层次数据转换成多级树形结构的JSON格式的字符串

2、对树中每一个层次的节点按照某一属性(比如分支机构编号)进行排序

下面介绍解决问题的思路:

在数据结构这门课中,我们都学过树,无限级树形结构就可以抽象成一种多叉树结构,即每个节点下包含多个子节点的树形结构,首先就需要把数据库中的层次数据转换成多叉树结构的对象树,也就是构造出一棵多叉树。

有了数据结构,还要实现相应的算法,我们需要实现两种算法:

1、兄弟节点横向排序算法,对隶属于同一个父节点下面的所有直接子节点按照某一节点属性和规则进行排序,保持兄弟节点横向有序;

2、先序遍历算法,递归打印出无限级JSON字符串。

概括起来分为三步:

1、构造无序的多叉树结构

2、实现兄弟节点横向排序方法

3、实现先序遍历方法,打印出JSON字符串

如图所示:

shujujiegou.jpg

三、源代码实现(Java版)

实现这样一颗树,需要设计两个类:树类(MultipleTree)、节点类(Node);排序时还需要一个比较器类(NodeIDComparator);为了方便演示,还需要构造一些假的层次数据,因此还需要建一个构造假数据的类(VirtualDataGenerator),以下代码拷贝出来之后可直接运行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
java复制代码package test;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Collections;

/**
* 多叉树类
*/
public class MultipleTree {
public static void main(String[] args) {
// 读取层次数据结果集列表
List dataList = VirtualDataGenerator.getVirtualResult();

// 节点列表(映射表,用于临时存储节点对象)
HashMap nodeList = new HashMap();
// 根节点
Node root = null;
// 将结果集存入映射表(后面将借助映射表构造多叉树)
for (Iterator it = dataList.iterator(); it.hasNext();) {
Map dataRecord = (Map) it.next();
Node node = new Node();
node.id = (String) dataRecord.get("id");
node.text = (String) dataRecord.get("text");
node.parentId = (String) dataRecord.get("parentId");
nodeList.put(node.id, node);
}
// 构造无序的多叉树
Set entrySet = nodeList.entrySet();
for (Iterator it = entrySet.iterator(); it.hasNext();) {
Node node = (Node) ((Map.Entry) it.next()).getValue();
if (node.parentId == null || node.parentId.equals("")) {
root = node;
} else {
((Node) nodeList.get(node.parentId)).addChild(node);
}
}
// 输出无序的树形结构的JSON字符串
System.out.println(root);
// 对多叉树进行横向排序
root.sortChildren();
// 输出有序的树形结构的JSON字符串
System.out.println(root);

// 程序输出结果如下:
//
// 无序的树形结构(格式化后的结果,可使用JSON格式化工具查看,例如 http://jsonviewer.stack.hu/ 在线查看器):
// {
// id : '100000',
// text : '廊坊银行总行',
// children : [
// {
// id : '110000',
// text : '廊坊分行',
// children : [
// {
// id : '113000',
// text : '廊坊银行开发区支行',
// leaf : true
// },
// {
// id : '111000',
// text : '廊坊银行金光道支行',
// leaf : true
// },
// {
// id : '112000',
// text : '廊坊银行解放道支行',
// children : [
// {
// id : '112200',
// text : '廊坊银行三大街支行',
// leaf : true
// },
// {
// id : '112100',
// text : '廊坊银行广阳道支行',
// leaf : true
// }
// ]
// }
// ]
// }
// ]
// }

// 有序的树形结构(格式化后的结果):
// {
// id : '100000',
// text : '廊坊银行总行',
// children : [
// {
// id : '110000',
// text : '廊坊分行',
// children : [
// {
// id : '111000',
// text : '廊坊银行金光道支行',
// leaf : true
// },
// {
// id : '112000',
// text : '廊坊银行解放道支行',
// children : [
// {
// id : '112100',
// text : '廊坊银行广阳道支行',
// leaf : true
// },
// {
// id : '112200',
// text : '廊坊银行三大街支行',
// leaf : true
// }
// ]
// },
// {
// id : '113000',
// text : '廊坊银行开发区支行',
// leaf : true
// }
// ]
// }
// ]
// }

}

}

/**
* 节点类
*/
class Node {
/**
* 节点编号
*/
public String id;

/**
* 节点内容
*/
public String text;

/**
* 父节点编号
*/
public String parentId;

/**
* 孩子节点列表
*/
private List children = new ArrayList();

// 添加孩子节点
public void addChild(Node node) {
children.add(node);
}

// 先序遍历,拼接JSON字符串
public String toString() {
String result = "{" + "id : '" + id + "'" + ", text : '" + text + "'";
if (children.size() != 0) {
result += ", children : [";
for (int i = 0; i < children.size(); i++) {
result += ((Node) children.get(i)).toString() + ",";
}
result = result.substring(0, result.length() - 1);
result += "]";
} else {
result += ", leaf : true";
}
return result + "}";
}

// 兄弟节点横向排序
public void sortChildren() {
if (children.size() != 0) {
// 对本层节点进行排序(可根据不同的排序属性,传入不同的比较器,这里 传入ID比较器)
Collections.sort(children, new NodeIDComparator());
// 对每个节点的下一层节点进行排序
for (int i = 0; i < children.size(); i++) {
((Node) children.get(i)).sortChildren();
}
}
}

}

/**
* 节点比较器
*/
class NodeIDComparator implements Comparator {
// 按照节点编号比较
public int compare(Object o1, Object o2) {
int j1 = Integer.parseInt(((Node) o1).id);
int j2 = Integer.parseInt(((Node) o2).id);
return (j1 < j2 ? -1 : (j1 == j2 ? 0 : 1));
}
}

/**
* 构造虚拟的层次数据
*/
class VirtualDataGenerator {
// 构造无序的结果集列表,实际应用中,该数据应该从数据库中查询获得;
public static List getVirtualResult() {
List dataList = new ArrayList();

HashMap dataRecord1 = new HashMap();
dataRecord1.put("id", "112000");
dataRecord1.put("text", "廊坊银行解放道支行");
dataRecord1.put("parentId", "110000");

HashMap dataRecord2 = new HashMap();
dataRecord2.put("id", "112200");
dataRecord2.put("text", "廊坊银行三大街支行");
dataRecord2.put("parentId", "112000");

HashMap dataRecord3 = new HashMap();
dataRecord3.put("id", "112100");
dataRecord3.put("text", "廊坊银行广阳道支行");
dataRecord3.put("parentId", "112000");

HashMap dataRecord4 = new HashMap();
dataRecord4.put("id", "113000");
dataRecord4.put("text", "廊坊银行开发区支行");
dataRecord4.put("parentId", "110000");

HashMap dataRecord5 = new HashMap();
dataRecord5.put("id", "100000");
dataRecord5.put("text", "廊坊银行总行");
dataRecord5.put("parentId", "");

HashMap dataRecord6 = new HashMap();
dataRecord6.put("id", "110000");
dataRecord6.put("text", "廊坊分行");
dataRecord6.put("parentId", "100000");

HashMap dataRecord7 = new HashMap();
dataRecord7.put("id", "111000");
dataRecord7.put("text", "廊坊银行金光道支行");
dataRecord7.put("parentId", "110000");

dataList.add(dataRecord1);
dataList.add(dataRecord2);
dataList.add(dataRecord3);
dataList.add(dataRecord4);
dataList.add(dataRecord5);
dataList.add(dataRecord6);
dataList.add(dataRecord7);

return dataList;
}
}

好了,通过上面的代码,就可以实现多叉树的兄弟节点横向排序和先序遍历了,实现了将层次数据转换为有序无限级树形结构JSON字符串的目的。

在实际的项目中,可以把上面的有效代码融入其中,或者在此基础上进行一些扩展:

1、实现对指定层次的排序(例如只排序第一层的节点,或者只排序某一父节点下的所有子节点)

2、遍历输出树形结构时可以加入判断条件过滤掉某些节点

3、实现节点的删除功能

4、在节点类中增加一个父节点的引用,就可以计算出某一节点所处的级别

5、在不支持层次查询的数据库应用系统中使用该算法实现相同的效果

四、思考与总结

这篇文章的重点是如何构造有序的无限级的树形结构JSON字符串,一次性生成树形结构,而不是利用Ajax的方式,反复向服务器端发送请求,一级接一级的加载树节点。

既然可以构造无限级的JSON字符串,那么也可以根据这个思路构造无限级的XML字符串,或者构造具有层次结构的UL – LI组合(用UL - LI来展示树形结构),或者构造具有层次结构的TABLE(用TABLE来展示树形结构)。

如下所示:

(1)XML层次结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
xml复制代码<nodeGroup id="100000" name="廊坊银行总行">
<nodeGroup id="110000" name="廊坊分行">
<node id="113000" name="廊坊银行开发区支行">
</node>
<node id="111000" name="廊坊银行金光道支行">
</node>
<nodeGroup id="112000" name="廊坊银行解放道支行">
<node id="112200" name="廊坊银行三大街支行">
</node>
<node id="112100" name="廊坊银行广阳道支行">
</node>
</nodeGroup>
</nodeGroup>
</nodeGroup>

(2)UL - LI 层次结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
html复制代码<ul>
<li>廊坊银行总行</li>
<ul>
<li>廊坊分行</li>
<ul>
<li>廊坊银行开发区支行</li>
<li>廊坊银行解放道支行</li>
<ul>
<li>廊坊银行三大街支行</li>
<li>廊坊银行广阳道支行</li>
</ul>
<li>廊坊银行金光道支行</li>
</ul>
</ul>
</ul>

(3)TABLE层次结构

1
2
3
4
5
6
7
8
9
html复制代码<table>
<tr><td>廊坊银行总行</td></tr>
<tr><td>&nbsp;&nbsp;廊坊分行</td></tr>
<tr><td>&nbsp;&nbsp;&nbsp;&nbsp;廊坊银行开发区支行</td></tr>
<tr><td>&nbsp;&nbsp;&nbsp;&nbsp;廊坊银行解放道支行</td></tr>
<tr><td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;廊坊银行三大街支行</td></tr>
<tr><td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;廊坊银行广阳道支行</td></tr>
<tr><td>&nbsp;&nbsp;&nbsp;&nbsp;廊坊银行金光道支行</td></tr>
</table>

另外对TreeGrid树形表格也有一定的价值:

1、一次性构造树形表格,实现数据分级展示

2、通过更换比较器,实现对不同表格列的全排序(全排序指的是对所有页的数据进行排序,而不是只对当前页的数据排序;排序规则与Oracle数据库中的层次查询类似,即兄弟节点横向排序)

3、实现对树形表格的完整分页(每次分页时,只取固定数目的第一层节点,之后调用toString方法,展示出完整条数的分级数据,即每页的记录条数是不固定的,但必须是完整的树形结构)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

java swing项目桌面软件还是蛮香的,至少有了我自己的

发表于 2021-06-05

这是我参与更文挑战的第5天,活动详情查看: 更文挑战

本文正在参加「Java主题月 - Java 开发实战」,详情查看 活动链接

最近利用空闲时间自己琢磨了一下java swing 编程,其实在从事javaweb之前我一直向往的就是java swing 开发,不知道为什么可能当时觉得Windows上的exe程序很是神奇,关于windows上制作exe我之前也有介绍

exe打包教程一

exe打包教程二

java swing开发都是自己琢磨的,有的地方写的不规范,不过大多都是网上借鉴的,应该不算离谱的。

今天看了看自己的java swing的程序,感觉写的还不错,但是发现现在遇到一个瓶颈问题,就是jtable的使用,由于一开始概念不理解现在jtable得重新写,之前我吧数据放在jtable上了,但是真正开发的java swing数据都是放在TableModel上的。下面就Jtable的使用,好好整理了一番,途中参考的文章我都会放在下面列出,读者可以自己参考**

JTable结构梳理

  • JTable=TableHeader+TableColumn
  • 顾名思义我们知道表格是由表头和表列组成的,这两个都是单独的控件。但是JTable中如果想让表头显示仅仅将JTable加入Jpanel或者Jframe中是不行的,我这里提供两种方式实现
  • 1、分别将TableHeader和TableColumn加入控件中单独的显示,这种情况不常见
  • 2、先将JTable加入jscrollpane(滚动条)中,然后在将滚动条加入到对应的控件中(Jpanel或者是Jframe).java swing 开发中加入滚动条是很常见的操作,所以这种方式的加入表格还是很推荐的。
1
2
3
4
5
6
ini复制代码table = new JTable(data, columnNames);
table.setBackground(Color.gray);
table.setPreferredScrollableViewportSize(new Dimension(800, 100));
table.setFillsViewportHeight(false);
pane = new JScrollPane(table);
this.add(pane);
  • 上面的代码就是简单的实现将表格以显示表头的方式加入到jframe中显示。其中我们能够看到多了几个设置,setPreferredScrollableViewportSize可有可无的,无所谓,但是setFillsViewportHeight是设置表格在纵向上的铺展情况,什么意思呢,如果这里设置为true则表格就会在纵向上铺满jframe,如果是false,表格则会按照自己的实际占地面积显示,不会多占的。

这里写图片描述

这里写图片描述

JTable数据显示

  • java swing中jtable是mvc形式的,所以jtable仅仅是数据的显示,而真正和数据绑定的却是TableModel这个接口,我们先看看这个接口的内部有哪些方法,这样我们心里才有个底。

这里写图片描述

  • 众多周知jtable中常用的两种构造函数一个是数组另一个是vector,这两种构造函数中都是采用了匿名内部类实现tablemodel,前者用的是AbstractTableModel,后者是DefaultTableModel。而DefaultTableModel有事继承了AbstractTableModel,所以我们平时如果自定义model的话,都会去继承AbstractTableModel的。我们在去源码里可以看见这里写图片描述,我们会发现AbstractTableModel有事继承TableModel这个接口的。所以我们的所有方法都是源于他。
  • 我们观察AbstractTableModel源码中注释发现,我们只需要继承AbstractTableModel类后只需要实现三个必须的方法,其他的方法根据需要实现
1
2
3
csharp复制代码public int getRowCount();
public int getColumnCount();
public Object getValueAt(int row, int column);

这里写图片描述

  • 这三个方法的作用就是通过model高数jtable我需要一个几行几列的表格,至于每个单元格显示的内容就是通过getValueAt这个方法实现的,到这里我们就实现了,jtable的model自定义显示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
csharp复制代码//表格的列,需要用户自己设定好
private int column;
//待加载的数据 数据每行的列数和上面要统一好
private List<Object> list;

public MyTableModel(List<Object> list,int column){
this.column=column;
this.list=list;
}
@Override
public int getRowCount() {
// TODO Auto-generated method stub
return list.size();
}

@Override
public int getColumnCount() {
// TODO Auto-generated method stub
return this.column;
}

@Override
public Object getValueAt(int rowIndex, int columnIndex) {
// TODO Auto-generated method stub
return list.get(rowIndex).toString()+"_"+rowIndex+"_"+columnIndex;
}
  • 上面的代码就是我的model类,里面有个穿list的构造函数,然后我们看看我们的三个方法就起到了作用了。然后在getValueAt获取指定行数的list的指定内容就可以填充到表格上了。

jtable数据的CURD

  • 在上面我们继承了AbstractTableModel,这里面有对数据的CURD操作

这里写图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
scss复制代码if("增加".equals(command)){
List<Object> data = getData();
data.add("test5");
data.add("test6");
datamModel.setList(data);
// datamModel.fireTableStructureChanged();
datamModel.fireTableRowsInserted(5,6);
}else if("删除".equals(command)){
List<Object> data = getData();
data.remove(2);
datamModel.setList(data);
datamModel.fireTableRowsDeleted(1, 1);
}else if("更新".equals(command)){
List<Object> data = getData();
data.set(0, "test0_1");
datamModel.setList(data);
datamModel.fireTableRowsUpdated(0, 5);
}
  • 里面都会带有参数比如datamModel.fireTableRowsUpdated(0, 5);的意思是通州JTable显示模块去刷新从1-6行的所有数据,所以说比如你更新了第一行的数据,而你用的两个参数是1,4.那么恭喜你,你的更新JTable无法实现,因为他只更新从第二行开始到第五行的数据。其他的方法参数和他一样。通过上面就可以轻松实现JTable的CURD操作。

JTable控件显示法

  • 相信有的朋友遇到过,表格中需要显示CheckBox、button等控件的,但是我们发现我们定义的控件在表格上显示成了控件对应的类的字符串了,这又是为什么呢。下面请随我一起看看源码吧:
1
2
3
4
5
6
7
8
9
vbnet复制代码/**
* Returns <code>Object.class</code> regardless of <code>columnIndex</code>.
*
* @param columnIndex the column being queried
* @return the Object.class
*/
public Class<?> getColumnClass(int columnIndex) {
return Object.class;
}
  • 源码中AbstractTableModel的getColumnClass的方法是JTable决定显示类型的主要依据,源码中返回的默认是object.class,这就意味着我们在jtable是true或false或其他类,在这里都会返回object.class,那么jtable就会采用默认的渲染方式去渲染表格,默认的就是所有都是字符串形式展示,这个设置在JTable中的源码中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
kotlin复制代码protected void createDefaultRenderers() {
defaultRenderersByColumnClass = new UIDefaults(8, 0.75f);

// Objects
setLazyRenderer(Object.class, "javax.swing.table.DefaultTableCellRenderer$UIResource");

// Numbers
setLazyRenderer(Number.class, "javax.swing.JTable$NumberRenderer");

// Doubles and Floats
setLazyRenderer(Float.class, "javax.swing.JTable$DoubleRenderer");
setLazyRenderer(Double.class, "javax.swing.JTable$DoubleRenderer");

// Dates
setLazyRenderer(Date.class, "javax.swing.JTable$DateRenderer");

// Icons and ImageIcons
setLazyRenderer(Icon.class, "javax.swing.JTable$IconRenderer");
setLazyRenderer(ImageIcon.class, "javax.swing.JTable$IconRenderer");

// Booleans
setLazyRenderer(Boolean.class, "javax.swing.JTable$BooleanRenderer");
}
  • 源码中就有渲染器专门处理bool类型的,那么我们只需要在getColumnClass进行处理就行了,在我们的bool的单元格返回bool类JTable就会采用bool类型的渲染器了。
1
scss复制代码return getValueAt(0, c).getClass();
  • 这样我们就可以显示一些其他的控件了,还有一些比如每行都需要一行按钮来实现修改改行的功能,这些按钮正常我们不需要再数据中添加(避免数据过于庞大),如果不在数据中添加那么我们的getClass方法就起不到作用,这个用到了下面的知识来解决

JTable渲染和编辑

  • 这种呢其实就是上面的getClass底层的实现方式,JTable中源码我们上面可以看出JTable通过返回的不同的类来调用不同的渲染器,现在我们不通过返回类的方式来渲染,而是自己定义一个渲染器,然后通过JTable提供的方法设定用该渲染器渲染该单元格!
1
复制代码setCellEditor和setCellRenderer
  • 大家可以观察源码,在JTable的编辑器中AbstractCellEditor是基础的抽象类,他继承了CellEditor,怎么样熟悉吗,这个不就是和AbstractTableModel 是一样的吗。但是这个类不能反回控件需要和TableCellEditor结合使用,或者我们只是用另外一个基础类DefaultCellEditor,
  • DefaultCellEditor和AbstractTableModel 有设么区别呢,他们都是一样实现了CellEditor接口,但是前者构造中只能传入控件,也就是说每日次实力只能通过不同构造函数构建不同的控件,但是后者是抽象类,继承的类可以自定义构造函数,这就方便我们够赞多个不同的控件了,所以这两个大家看情况使用。最后都是通过getTableCellEditorComponent这个函数将控件返回出去。
  • 设置完了编辑器,我们最终要是只渲染器,就是JTable最终如何显示的问题。和上面的那个一样。继承TableCellRenderer类,通过getTableCellRendererComponent方法返回渲染成设么控件,渲染的控件我们可以进行二次封装。
  • 调用如下 两个参数就是通过上面两个类构造的类
1
2
3
4
ini复制代码column.setCellEditor(editor);


column.setCellRenderer(renderer);

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

手把手教你实现分布式服务的调用链跟踪实现

发表于 2021-06-05

什么是分布式服务的调用链跟踪?

分布式服务中完成某一个业务动作,需要服务之间的相互协作才能完成,在这一次动作引起的多服务的联动我们需要用1个唯一标识关联起来,关联起来就是调用链的跟踪。

为什么要实现调用链跟踪?

随着业务的发展,所有的系统最终都会走向服务化体系,微服务的目的一是提高系统的稳定性,二是提高持续交付的效率,为什么能提高这两项不是今天讨论的内容。

当然这也不是绝对的,如果业务还在MVP验证,团队规模小个人觉得完全没必要微服务化、单体应用是比较好的选择。作者是有经历过从单体应用到1000+应用的增长经历,也是见证了公司从初创到上市的过程,对于系统阶段和业务阶段的匹配还是有比较深的感受的。

服务拆分后带来的问题是什么呢?服务的依赖关系复杂后,对于问题的排查也增加了复杂度,当然站在更高的角度来看拆分带来的不只是排错复杂性的提升,工程效率、组织协作也都会带来新的挑战。

回到主题,如何快速查询整个请求链路上的日志并呈现出来是解决排查问题复杂度的根本方法,这就是今天我们要讲的内容,如何自己来实现一个全链路跟踪。

如何实现?

第一步,看图、看场景,用户浏览器的一次请求行为所走的路径是什么样的

image.png

如上图、省略了4层和7层的LB,请求直接到gateway->A->B 那如何把个request关联起来呢?从时序上来看我们只要在gateway生成一个traceId然后层层透传,那么每一次的request的我们就能通过traceid关联查询出来了。

如何透传、如何记录呢?或者说如何透传、如何记录让各应用的开发人员无需关注呢?

第二步,实现。不想看代码可直接拉最后看结果和原理

如何传递,这里我们使用定义统一的Request类,所有的api层需要使用这个规范,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class Request<T> implements Serializable {
//header:携带需要传递的信息
private RequestHeader header;
//业务参数
private T bizModel;
//...省略get set
}
public class RequestHeader implements Serializable {

//调用链唯一ID
private String traceId;
//当前用户Id
private String userId;
//上游调用方appId
private String callAppId;
//...省略get set
}

有了这个Request之后,我们在网关层每次都生成traceId, 然后在各服务之间传递就能做到调用链的关联了。我们继续看个各应用应该如何定义服务和使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码    @ApiMethod
@PostMapping("/test")
@ApiOperation(value = "test", notes = "", response = String.class)
public Response<ExampleRespDTO> test(@RequestBody Request<ExampleReqDTO> req) {
ExampleRespDTO exampleRespDTO = new ExampleRespDTO();
exampleRespDTO.setName(req.getBizModel().getName());

//输出当前应用的header信息
System.out.println("上游的traceId:"+RequestContext.getHeader().getTraceId());
System.out.println("上游的callAppId:"+RequestContext.getHeader().getCallAppId());
System.out.println("上游的userId:"+RequestContext.getHeader().getUserId());


/***
* 模拟调用其他应用服务
* 通过RPCRequest 来构建request对象
*/
Request<OtherAppServiceReqDTO> otherAppServiceReqDTORequest =RPCRequest.createRequest(new OtherAppServiceReqDTO());

//输出下游应用的header信息
System.out.println("调用下游的traceId:"+otherAppServiceReqDTORequest.getHeader().getTraceId());
System.out.println("调用下游的callAppId:"+otherAppServiceReqDTORequest.getHeader().getCallAppId());
System.out.println("调用下游的userId:"+otherAppServiceReqDTORequest.getHeader().getUserId());

return Response.successResponse(exampleRespDTO);
}

看完上面代码的同学,应该看到了有一个模拟调用其他服务的地方,这里主要解决的是服务和服务之间的调用header传递的问题,这里封装了一个createRequest的方法,其主要内容还是把当前应用的requestHeader 赋值给请求其他服务的request上。这也是一个测试接口,最后面有测试的结果

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class RPCRequest {
public static <T> Request<T> createRequest(T requestData){
Request<T> request = new Request();
RequestHeader requestHeader=new RequestHeader();
requestHeader.setTraceId(RequestContext.getHeader().getTraceId());
requestHeader.setUserId(RequestContext.getHeader().getUserId());
requestHeader.setCallAppId(AppConfig.CURRENT_APP_ID);
request.setHeader(requestHeader);
request.setBizModel(requestData);
return request;
}
}

当前request中的header存在什么地方呢,我们看一下RequestContext的代码

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class RequestContext {
private static ThreadLocal<RequestHeader> threadLocal=new ThreadLocal<>();
public static void setHeader(RequestHeader header){
threadLocal.set(header);
}
public static RequestHeader getHeader(){
return threadLocal.get();
}
public static void clear(){
threadLocal.remove();
}
}

header是什么时候放进去的呢?这里就是AOP该发挥作用的时候了,直接看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码public class ApiHandler {
public ApiHandler() {
}

public Response handleApiMethod(ProceedingJoinPoint pjp, ApiMethod apiMethod) {
//获取上游调用方的request header
Object[] args = pjp.getArgs();
Request request = (Request) args[0];
RequestHeader header = request.getHeader();
//将header加入到当前request 到ThreadLocal保存
RequestContext.setHeader(header);
Response response = null;
try {
//构建response header
ResponseHeader responseHeader = new ResponseHeader();
responseHeader.setTraceId(RequestContext.getHeader().getTraceId());
//执行service方法
response = (Response) pjp.proceed(args);
response.setHeader(responseHeader);

} catch (Throwable throwable) {
throwable.printStackTrace();
}finally {
//清除ThreadLocal中当前请求的header 对象
RequestContext.clear();
}
return response;

}
}

不想看代码的,直接看下图,原理比较简单,浅黄色为AOP作用,接口执行前和执行后,其中reqeuest和header的定义在第1段代码

image.png

这里没有介绍如何收集数据和查询展示,比较简单的办法是使用logback打本地日志,然后通过agent抽到集中式日志进行查询展示,例如ELK。

测试一下结果:

1、接口文档

image.png

2、执行结果

image.png

image.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

ELK搭建-生产级|Java 开发实战 1话不多说,直接开

发表于 2021-06-04

【话不多说】

1.话不多说,直接开干,多次搭建,准确无误

下载不了,请到官网下载!!!

wget artifacts.elastic.co/downloads/e…
wget artifacts.elastic.co/downloads/k…
wget artifacts.elastic.co/downloads/b…
wget artifacts.elastic.co/downloads/l…

tar -xzvf elasticsearch-6.7.2.tar.gz -C /usr/local/
tar -xzvf kibana-6.7.2-linux-x86_64.tar.gz -C /usr/local/
tar -xzvf filebeat-6.7.2-linux-x86_64.tar.gz -C /usr/local/
tar -xzvf logstash-6.7.2.tar.gz -C /usr/local/

首先安装jdk环境
#############################################################

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码rpm -ivh jdk-8u202-linux-x64.rpm
pid="sed -i '/export JAVA_HOME/d' /etc/profile"
eval $pid
pid="sed -i '/export CLASSPATH/d' /etc/profile"
eval $pid
cat >> /etc/profile <<EOF
export JAVA_HOME=/usr/java/jdk1.8.0_152
export CLASSPATH=%JAVA_HOME%/lib:%JAVA_HOME%/jre/lib
export PATH=\$PATH:\$JAVA_HOME/bin
EOF
source /etc/profile
java -version

#############################################################
#给elasticsearch用户授权
groupadd elasticsearch
useradd elasticsearch -g elasticsearch
chown -R elasticsearch.elasticsearch /usr/local/elasticsearch-6.7.2
chown -R elasticsearch.elasticsearch /usr/local/kibana-6.7.2-linux-x86_64/config
chown -R elasticsearch.elasticsearch /usr/local/filebeat-6.7.2-linux-x86_64
hostnamectl set-hostname elk-server
systemctl stop firewalld.service
systemctl disable firewalld.service

1
2
3
4
5
6
7
8
9
10
bash复制代码cat >> /etc/security/limits.conf << EOF
* soft nofile 65536
* hard nofile 131072
* soft nproc 2048
* hard nproc 4096
EOF

cat >> /etc/sysctl.conf << EOF
vm.max_map_count=655360
EOF

sysctl -p

su - elasticsearch

/usr/local/elasticsearch-6.7.2/bin/elasticsearch -d

vi /usr/local/elasticsearch-6.7.2/config/elasticsearch.yml 修改network:0.0.0.0

curl http://127.0.0.1:9200

vi /usr/local/logstash-6.7.2/config/logstash.yml

需要root创建给elasticsearch用户权限

#################################################

path.data: /data/logstash/data

path.logs: /data/logstash/logs

#################################################

vi /usr/local/logstash-6.7.2/default.conf

#################################################

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ini复制代码input {
beats {
host => "192.168.244.200"
port => 5044
codec => plain {
charset => "UTF-8"
}
}
}

output {
elasticsearch {
hosts => "127.0.0.1:9200"
manage_template => false
index => "%{[@metadata][beat]}-%{+YYYY.MM.dd}"
document_type => "%{[@metadata][type]}"
}
}

##########################################
根据自己需要更改

1
2
3
4
bash复制代码vi /usr/local/logstash-6.7.2/config/jvm.options       # 修改jvm内存 1g
vi /usr/local/elasticsearch-6.7.2/config/jvm.options # 修改jvm内存 1g
nohup /usr/local/logstash-6.7.2/bin/logstash -f /usr/local/logstash-6.7.2/default.conf --config.reload.automatic > logstash.log 2>&1 &
vi /usr/local/kibana-6.7.2-linux-x86_64/config/kibana.yml

############################################

1
2
3
yaml复制代码server.port: 5601
server.host: "192.168.2.207" 云服务器0.0.0.0
elasticsearch.url: "http://localhost:9200"

############################################

1
bash复制代码nohup /usr/local/kibana-6.7.2-linux-x86_64/bin/kibana > kibana.log 2>&1 &

#// 通过filebeat收集日志,发送到logstash(生产一般在这里配置log的地址)

1
bash复制代码vi /usr/local/filebeat-6.7.2-linux-x86_64/filebeat.yml

############################################

1
2
3
4
5
6
7
8
lua复制代码filebeat.prospectors:
- type: log
enabled: true
paths:
- /var/log/*.log
output.logstash:
hosts: ["localhost:5044"]
注释掉output.elasticsearch

############################################

1
bash复制代码nohup /usr/local/filebeat-6.7.2-linux-x86_64/filebeat -e -c /usr/local/filebeat-6.7.2-linux-x86_64/filebeat.yml -d "publish" > filebeat.log 2>&1 &

#最后,多看看su - elasticsearch里面的3个日志报错。

1,一般没出日志都是filebeat没配置好logstash,或是elsasearch、logstash状态有问题,或是配置文件错误导致

2,一般日志找不到有可能是时间戳无法分隔的问题

3,Docker目录位置:- /var/new_lib/docker/containers//-json.log

4,服务器时间与真实时间不一致

时间修正:root 用户

1
2
3
4
5
6
7
8
bash复制代码#######################
yum install -y ntpdate
yes | cp -f /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
ntpdate us.pool.ntp.org
crontab -l >/tmp/crontab.bak
echo "*/10 * * * * /usr/sbin/ntpdate us.pool.ntp.org | logger -t NTP" >> /tmp/crontab.bak
crontab /tmp/crontab.bak
#######################

参考网址:

blog.51cto.com/andyxu/2124…

blog.csdn.net/boling_cava…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

通过java动态填pdf充表单数据

发表于 2021-06-04

使用java通过pdf 模板动态填充数据

1、准备pdf 模板文件

使用表单对pdf 文件进行编辑,

1622804107700.png

黑色框框是表单域,即java 代码中使用map的key ,通过key 将map中key 对应的value填充到表单中

1622804169591.png

2、java 代码

工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
java复制代码package com.creditease.collection.common.utils;

import com.itextpdf.text.Document;
import com.itextpdf.text.pdf.*;
import org.apache.commons.io.IOUtils;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

/**
* pdf 处理工具类:用于填充pdf 模板数据使用
* * 依赖的包:itextpdf itext-asian
* * commons-io,commons-codec
*/
public class PdfUtils {
/**
* Description: 使用map中的参数填充pdf,map中的key和pdf表单中的field对应 <br>
* @Param fieldValueMap:文件字段 file:源文件的字节流 contractFileName:生成目标文件存放的地址
* @return
*/
public static void fillParam(Map<String, String> fieldValueMap, byte[] file, String contractFileName) {
FileOutputStream fos = null;
try {
//生成pdf 的路径
fos = new FileOutputStream(contractFileName);
PdfReader reader = null;
PdfStamper stamper = null;
BaseFont base;
try {
reader = new PdfReader(file);
stamper = new PdfStamper(reader, fos);
stamper.setFormFlattening(true);
base = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
AcroFields acroFields = stamper.getAcroFields();
for (String key : acroFields.getFields().keySet()) {
//设置一些填充位置的字体属性
acroFields.setFieldProperty(key, "textfont", base, null);
acroFields.setFieldProperty(key, "textsize", new Float(9), null);
}
if (fieldValueMap != null) {
//将map填充到对应的位置中
for (String fieldName : fieldValueMap.keySet()) {
acroFields.setField(fieldName, fieldValueMap.get(fieldName));
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (stamper != null) {
try {
stamper.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if (reader != null) {
reader.close();
}
}

} catch (Exception e) {
System.out.println("填充参数异常");
e.printStackTrace();
} finally {
IOUtils.closeQuietly(fos);
}
}

/**
* Description: 获取pdf表单中的fieldNames<br>
* @author mk
* @Date 2018-11-2 15:21 <br>
* @Param
* @return
*/
public static Set<String> getTemplateFileFieldNames(String pdfFileName) {
Set<String> fieldNames = new TreeSet<String>();
PdfReader reader = null;
try {
reader = new PdfReader(pdfFileName);
Set<String> keys = reader.getAcroFields().getFields().keySet();
for (String key : keys) {
int lastIndexOf = key.lastIndexOf(".");
int lastIndexOf2 = key.lastIndexOf("[");
fieldNames.add(key.substring(lastIndexOf != -1 ? lastIndexOf + 1 : 0, lastIndexOf2 != -1 ? lastIndexOf2 : key.length()));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
reader.close();
}
}

return fieldNames;
}


/**
* Description: 读取文件数组<br>
* @author mk
* @Date 2018-11-2 15:21 <br>
* @Param
* @return
*/
public static byte[] fileBuff(String filePath) throws IOException {
File file = new File(filePath);
long fileSize = file.length();
if (fileSize > Integer.MAX_VALUE) {
//System.out.println("file too big...");
return null;
}
FileInputStream fi = new FileInputStream(file);
byte[] file_buff = new byte[(int) fileSize];
int offset = 0;
int numRead = 0;
while (offset < file_buff.length && (numRead = fi.read(file_buff, offset, file_buff.length - offset)) >= 0) {
offset += numRead;
}
// 确保所有数据均被读取
if (offset != file_buff.length) {
throw new IOException("Could not completely read file " + file.getName());
}
fi.close();
return file_buff;
}

/**
* Description: 合并pdf <br>
* @author mk
* @Date 2018-11-2 15:21 <br>
* @Param
* @return
*/
public static void mergePdfFiles(String[] files, String savepath) {
Document document = null;
try {
document = new Document(); //默认A4大小
PdfCopy copy = new PdfCopy(document, new FileOutputStream(savepath));
document.open();
for (int i = 0; i < files.length; i++) {
PdfReader reader = null;
try {
reader = new PdfReader(files[i]);
int n = reader.getNumberOfPages();
for (int j = 1; j <= n; j++) {
document.newPage();
PdfImportedPage page = copy.getImportedPage(reader, j);
copy.addPage(page);
}
} finally {
if (reader != null) {
reader.close();
}
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//关闭PDF文档流,OutputStream文件输出流也将在PDF文档流关闭方法内部关闭
if (document != null) {
document.close();
}

}
}


}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
java复制代码package com.creditease.collection.service.YRDEvidence;

import com.creditease.collection.common.utils.PdfUtils;
import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;

public class TestPdf {
public static void main(String[] args) {
HashMap map = new HashMap<String, String>();
map.put("name", "杨杰");
map.put("sex", "nv");
map.put("age", "27");
map.put("phone", "15521331");
map.put("email", "812406fdf@qq.com");
map.put("idCard", "4305223243434332");
map.put("hobby", "跑步");
map.put("time", "2019年5月22日");
//源模板地址
String sourceFile = "C:\\Users\\Administrator\\Desktop\\工作笔记\\20210602\\还款记录Pdf\\还款记录模板.pdf";
//生成文件的目标地址
String targetFile = "C:\\Users\\Administrator\\Desktop\\工作笔记\\20210602\\还款记录Pdf\\test.pdf";
File templateFile = new File(sourceFile);
try {
/* 获取模板文件中的所有表单域
Set<String> templateFileFieldNames = PdfUtils.getTemplateFileFieldNames(sourceFile);
System.out.println(templateFileFieldNames);*/

PdfUtils.fillParam(map, FileUtils.readFileToByteArray(templateFile), targetFile);
} catch (IOException e) {
e.printStackTrace();
}
}
}

生成的文件

1622804542501.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

万字长文!一文彻底搞懂Java多线程 前言 一、创建线程和启

发表于 2021-06-04

前言

线程对象是可以产生线程的对象。比如在Java平台中Thread对象,Runnable对象。线程,是指正在执行的一个指点令序列。在java平台上是指从一个线程对象的start()开始,运行run方法体中的那一段相对独立的过程。相比于多进程,多线程的优势有:

(1)进程之间不能共享数据,线程可以;


(2)系统创建进程需要为该进程重新分配系统资源,故创建线程代价比较小;


(3)Java语言内置了多线程功能支持,简化了java多线程编程。

一、创建线程和启动

(1)继承Thread类创建线程类

通过继承Thread类创建线程类的具体步骤和具体代码如下:

• 定义一个继承Thread类的子类,并重写该类的run()方法;

• 创建Thread子类的实例,即创建了线程对象;

• 调用该线程对象的start()方法启动线程。

1
2
3
4
5
6
7
8
9
10
11
scala复制代码 class SomeThead extends Thraad   { 
public void run() {
//do something here
}
}

public static void main(String[] args){
SomeThread oneThread = new SomeThread();
步骤3:启动线程:
oneThread.start();
}
(2)实现Runnable接口创建线程类

通过实现Runnable接口创建线程类的具体步骤和具体代码如下:

• 定义Runnable接口的实现类,并重写该接口的run()方法;

• 创建Runnable实现类的实例,并以此实例作为Thread的target对象,即该Thread对象才是真正的线程对象。

1
2
3
4
5
6
7
8
java复制代码class SomeRunnable implements Runnable   { 
public void run() {
//do something here
}
}
Runnable oneRunnable = new SomeRunnable();
Thread oneThread = new Thread(oneRunnable);
oneThread.start();
(3)通过Callable和Future创建线程

通过Callable和Future创建线程的具体步骤和具体代码如下:

• 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
• 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
• 使用FutureTask对象作为Thread对象的target创建并启动新线程。
• 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值其中,Callable接口(也只有一个方法)定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
ini复制代码public interface Callable   { 
V call() throws Exception;
}
步骤1:创建实现Callable接口的类SomeCallable(略);
步骤2:创建一个类对象:
Callable oneCallable = new SomeCallable();
步骤3:由Callable创建一个FutureTask对象:
FutureTask oneTask = new FutureTask(oneCallable);
注释: FutureTask是一个包装器,它通过接受Callable来创建,它同时实现了 Future和Runnable接口。
步骤4:由FutureTask创建一个Thread对象:
Thread oneThread = new Thread(oneTask);
步骤5:启动线程:
oneThread.start();

二、线程的生命周期

image.png

1、新建状态

用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态(runnable)。

注意:不能对已经启动的线程再次调用start()方法,否则会出现Java.lang.IllegalThreadStateException异常。

2、就绪状态

处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为cpu的调度不一定是按照先进先出的顺序来调度的),等待系统为其分配CPU。等待状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态,系统挑选的动作称之为“cpu调度”。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。

提示:如果希望子线程调用start()方法后立即执行,可以使用Thread.sleep()方式使主线程睡眠一会儿,转去执行子线程。

3、运行状态

处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

处于就绪状态的线程,如果获得了cpu的调度,就会从就绪状态变为运行状态,执行run()方法中的任务。如果该线程失去了cpu资源,就会又从运行状态变为就绪状态。重新等待系统分配资源。也可以对在运行状态的线程调用yield()方法,它就会让出cpu资源,再次变为就绪状态。

注: 当发生如下情况时,线程会从运行状态变为阻塞状态:

①、线程调用sleep方法主动放弃所占用的系统资源


②、线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞


③、线程试图获得一个同步监视器,但更改同步监视器正被其他线程所持有


④、线程在等待某个通知(notify)


⑤、程序调用了线程的suspend方法将线程挂起。不过该方法容易导致死锁,所以程序应该尽量避免使用该方法。

当线程的run()方法执行完,或者被强制性地终止,例如出现异常,或者调用了stop()、desyory()方法等等,就会从运行状态转变为死亡状态。

4、阻塞状态

处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。 

在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。有三种方法可以暂停Threads执行:

5、死亡状态

当线程的run()方法执行完,或者被强制性地终止,就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。 如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

三、线程管理

Java提供了一些便捷的方法用于会线程状态的控制。具体如下:

1、线程睡眠——sleep

如果我们需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread的sleep方法。

注:

(1)sleep是静态方法,最好不要用Thread的实例对象调用它,因为它睡眠的始终是当前正在运行的线程,而不是调用它的线程对象,它只对正在运行状态的线程对象有效。如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
scss复制代码public class Test1 {  
public static void main(String[] args) throws InterruptedException {
System.out.println(Thread.currentThread().getName());
MyThread myThread=new MyThread();
myThread.start();
myThread.sleep(1000);//这里sleep的就是main线程,而非myThread线程
Thread.sleep(10);
for(int i=0;i<100;i++){
System.out.println("main"+i);
}
}
}
(2)Java线程调度是Java多线程的核心,只有良好的调度,才能充分发挥系统的性能,提高程序的执行效率。但是不管程序员怎么编写调度,只能最大限度地影响线程执行的次序,而不能做到精准控制。因为使用sleep方法之后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会重新进入到就绪状态,而就绪状态进入到运行状态,是由系统控制的,我们不可能精准地去干涉它,所以如果调用Thread.sleep(1000)使得线程睡眠1秒,可能结果会大于1秒。

2、线程让步——yield

yield()方法和sleep()方法有点相似,它也是Thread类提供的一个静态的方法,它也可以让当前正在执行的线程暂停,让出cpu资源给其他的线程。但是和sleep()方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态。yield()方法只是让当前线程暂停一下,重新进入就绪的线程池中,让系统的线程调度器重新调度器重新调度一次,完全可能出现这样的情况:当某个线程调用yield()方法之后,线程调度器又将其调度出来重新进入到运行状态执行。

实际上,当某个线程调用了yield()方法暂停之后,优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程更有可能获得执行的机会,当然,只是有可能,因为我们不可能精确的干涉cpu调度线程。用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public class Test1 {  
public static void main(String[] args) throws InterruptedException {
new MyThread("低级", 1).start();
new MyThread("中级", 5).start();
new MyThread("高级", 10).start();
}
}

class MyThread extends Thread {
public MyThread(String name, int pro) {
super(name);// 设置线程的名称
this.setPriority(pro);// 设置优先级
}

@Override
public void run() {
for (int i = 0; i < 30; i++) {
System.out.println(this.getName() + "线程第" + i + "次执行!");
if (i % 5 == 0)
Thread.yield();
}
}
}

注:关于sleep()方法和yield()方的区别如下:

①、sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态。

②、sleep方法声明抛出了InterruptedException,所以调用sleep方法的时候要捕获该异常,或者显示声明抛出该异常。而yield方法则没有声明抛出任务异常。

③、sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法来控制并发线程的执行。

3、线程合并——join

线程的合并的含义就是将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行时,Thread类提供了join方法来完成这个功能,注意,它不是静态方法。
从上面的方法的列表可以看到,它有3个重要的方法:

1
2
3
4
5
6
arduino复制代码void join()      
当前线程等该加入该线程后面,等待该线程终止。
void join(long millis)
当前线程等待该线程终止的时间最长为 millis 毫秒。 如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度
void join(long millis,int nanos)
等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度

4、设置线程的优先级

每个线程执行时都有一个优先级的属性,优先级高的线程可以获得较多的执行机会,而优先级低的线程则获得较少的执行机会。与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的也并非没机会执行。

每个线程默认的优先级都与创建它的父线程具有相同的优先级,在默认情况下,main线程具有普通优先级。

注:Thread类提供了setPriority(int newPriority)和getPriority()方法来设置和返回一个指定线程的优先级,其中setPriority方法的参数是一个整数,范围是1~·0之间,也可以使用Thread类提供的三个静态常量:

1
2
3
4
5
ini复制代码MAX_PRIORITY   =10

MIN_PRIORITY =1

NORM_PRIORITY =5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码    public class Test1 {  
public static void main(String[] args) throws InterruptedException {
new MyThread("高级", 10).start();
new MyThread("低级", 1).start();
}
}

class MyThread extends Thread {
public MyThread(String name,int pro) {
super(name);//设置线程的名称
setPriority(pro);//设置线程的优先级
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(this.getName() + "线程第" + i + "次执行!");
}
}
}

注:虽然Java提供了10个优先级别,但这些优先级别需要操作系统的支持。不同的操作系统的优先级并不相同,而且也不能很好的和Java的10个优先级别对应。所以我们应该使用MAX_PRIORITY、MIN_PRIORITY和NORM_PRIORITY三个静态常量来设定优先级,这样才能保证程序最好的可移植性。

5、后台(守护)线程

守护线程使用的情况较少,但并非无用,举例来说,JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。调用线程对象的方法setDaemon(true),则可以将其设置为守护线程。守护线程的用途为:


• 守护线程通常用于执行一些后台作业,例如在你的应用程序运行时播放背景音乐,在文字编辑器里做自动语法检查、自动保存等功能。


• Java的垃圾回收也是一个守护线程。守护线的好处就是你不需要关心它的结束问题。例如你在你的应用程序运行的时候希望播放背景音乐,如果将这个播放背景音乐的线程设定为非守护线程,那么在用户请求退出的时候,不仅要退出主线程,还要通知播放背景音乐的线程退出;如果设定为守护线程则不需要了。

setDaemon方法的详细说明:

1
2
3
4
5
6
7
java复制代码public final void setDaemon(boolean on)        将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。    
该方法必须在启动线程前调用。 该方法首先调用该线程的 checkAccess 方法,且不带任何参数。这可能抛出 SecurityException(在当前线程中)。
参数:
on - 如果为 true,则将该线程标记为守护线程。
抛出:
IllegalThreadStateException - 如果该线程处于活动状态。
SecurityException - 如果当前线程无法修改该线程。

注:JRE判断程序是否执行结束的标准是所有的前台执线程行完毕了,而不管后台线程的状态,因此,在使用后台县城时候一定要注意这个问题。

6、正确结束线程

Thread.stop()、Thread.suspend、Thread.resume、Runtime.runFinalizersOnExit这些终止线程运行的方法已经被废弃了,使用它们是极端不安全的!想要安全有效的结束一个线程,可以使用下面的方法:

• 正常执行完run方法,然后结束掉;


• 控制循环条件和判断条件的标识符来结束掉线程。
1
2
3
4
5
6
7
8
9
10
11
12
13
scala复制代码class MyThread extends Thread {  
int i=0;
boolean next=true;
@Override
public void run() {
while (next) {
if(i==10)
next=false;
i++;
System.out.println(i);
}
}
}

四、线程同步

java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。

1、同步方法

即有synchronized关键字修饰的方法。由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
1
arduino复制代码public synchronized void save(){}

注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类

2、同步代码块

即有synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
csharp复制代码    public class Bank {  

        private int count =0;//账户余额  

        //存钱  
        public   void addMoney(int money){  

            synchronized (this) {  
                count +=money;  
            }  
            System.out.println(System.currentTimeMillis()+"存进:"+money);  
        }  

        //取钱  
        public   void subMoney(int money){  

            synchronized (this) {  
                if(count-money < 0){  
                    System.out.println("余额不足");  
                    return;  
                }  
                count -=money;  
            }  
            System.out.println(+System.currentTimeMillis()+"取出:"+money);  
        }  

        //查询  
        public void lookMoney(){  
            System.out.println("账户余额:"+count);  
        }
    }

注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

3、使用特殊域变量(volatile)实现线程同步

• volatile关键字为域变量的访问提供了一种免锁机制;

• 使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新;

• 因此每次使用该域就要重新计算,而不是使用寄存器中的值;

• volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
java复制代码 public class SynchronizedThread {

class Bank {

private volatile int account = 100;

public int getAccount() {
return account;
}

/**
* 用同步方法实现
*
* @param money
*/
public synchronized void save(int money) {
account += money;
}

/**
* 用同步代码块实现
*
* @param money
*/
public void save1(int money) {
synchronized (this) {
account += money;
}
}
}

class NewThread implements Runnable {
private Bank bank;

public NewThread(Bank bank) {
this.bank = bank;
}

@Override
public void run() {
for (int i = 0; i < 10; i++) {
// bank.save1(10);
bank.save(10);
System.out.println(i + "账户余额为:" +bank.getAccount());
}
}

}

/**
* 建立线程,调用内部类
*/
public void useThread() {
Bank bank = new Bank();
NewThread new_thread = new NewThread(bank);
System.out.println("线程1");
Thread thread1 = new Thread(new_thread);
thread1.start();
System.out.println("线程2");
Thread thread2 = new Thread(new_thread);
thread2.start();
}

public static void main(String[] args) {
SynchronizedThread st = new SynchronizedThread();
st.useThread();
}

注:多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。用final域,有锁保护地域和volatile域可以避免非同步的问题。

4、使用重入锁(Lock)实现线程同步

在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。ReenreantLock类的常用方法有:    
1
2
3
scss复制代码 ReentrantLock() : 创建一个ReentrantLock实例         
lock() : 获得锁
unlock() : 释放锁

注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
csharp复制代码//只给出要修改的代码,其余代码与上同
class Bank {

private int account = 100;
//需要声明这个锁
private Lock lock = new ReentrantLock();
public int getAccount() {
return account;
}
//这里不再需要synchronized
public void save(int money) {
lock.lock();
try{
account += money;
}finally{
lock.unlock();
}

}
}

五、线程通信

1、借助于Object类的wait()、notify()和notifyAll()实现通信

线程执行wait()后,就放弃了运行资格,处于冻结状态;

线程运行时,内存中会建立一个线程池,冻结状态的线程都存在于线程池中,notify()执行时唤醒的也是线程池中的线程,线程池中有多个线程时唤醒第一个被冻结的线程。
notifyall(), 唤醒线程池中所有线程。 注: (1) wait(), notify(),notifyall()都用在同步里面,因为这3个函数是对持有锁的线程进行操作,而只有同步才有锁,所以要使用在同步中;
(2) wait(),notify(),notifyall(), 在使用时必须标识它们所操作的线程持有的锁,因为等待和唤醒必须是同一锁下的线程;而锁可以是任意对象,所以这3个方法都是Object类中的方法。

单个消费者生产者例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
typescript复制代码 class Resource{  //生产者和消费者都要操作的资源  
private String name;
private int count=1;
private boolean flag=false;
public synchronized void set(String name){
if(flag)
try{wait();}catch(Exception e){}
this.name=name+"---"+count++;
System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
flag=true;
this.notify();
}
public synchronized void out(){
if(!flag)
try{wait();}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);
flag=false;
this.notify();
}
}
class Producer implements Runnable{
private Resource res;
Producer(Resource res){
this.res=res;
}
public void run(){
while(true){
res.set("商品");
}
}
}
class Consumer implements Runnable{
private Resource res;
Consumer(Resource res){
this.res=res;
}
public void run(){
while(true){
res.out();
}
}
}
public class ProducerConsumerDemo{
public static void main(String[] args){
Resource r=new Resource();
Producer pro=new Producer(r);
Consumer con=new Consumer(r);
Thread t1=new Thread(pro);
Thread t2=new Thread(con);
t1.start();
t2.start();
}
}//运行结果正常,生产者生产一个商品,紧接着消费者消费一个商品。

但是如果有多个生产者和多个消费者,上面的代码是有问题,比如2个生产者,2个消费者,运行结果就可能出现生产的1个商品生产了一次而被消费了2次,或者连续生产2个商品而只有1个被消费,这是因为此时共有4个线程在操作Resource对象r, 而notify()唤醒的是线程池中第1个wait()的线程,所以生产者执行notify()时,唤醒的线程有可能是另1个生产者线程,这个生产者线程从wait()中醒来后不会再判断flag,而是直接向下运行打印出一个新的商品,这样就出现了连续生产2个商品。 为了避免这种情况,修改代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
ini复制代码    class Resource{  
private String name;
private int count=1;
private boolean flag=false;
public synchronized void set(String name){
while(flag) /*原先是if,现在改成while,这样生产者线程从冻结状态醒来时,还会再判断flag.*/
try{wait();}catch(Exception e){}
this.name=name+"---"+count++;
System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
flag=true;
this.notifyAll();/*原先是notity(), 现在改成notifyAll(),这样生产者线程生产完一个商品后可以将等待中的消费者线程唤醒,否则只将上面改成while后,可能出现所有生产者和消费者都在wait()的情况。*/
}
public synchronized void out(){
while(!flag) /*原先是if,现在改成while,这样消费者线程从冻结状态醒来时,还会再判断flag.*/
try{wait();}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);
flag=false;
this.notifyAll(); /*原先是notity(), 现在改成notifyAll(),这样消费者线程消费完一个商品后可以将等待中的生产者线程唤醒,否则只将上面改成while后,可能出现所有生产者和消费者都在wait()的情况。*/
}
}
public class ProducerConsumerDemo{
public static void main(String[] args){
Resource r=new Resource();
Producer pro=new Producer(r);
Consumer con=new Consumer(r);
Thread t1=new Thread(pro);
Thread t2=new Thread(con);
Thread t3=new Thread(pro);
Thread t4=new Thread(con);
t1.start();
t2.start();
t3.start();
t4.start();
}
}

2、使用Condition控制线程通信

 jdk1.5中,提供了多线程的升级解决方案为:


(1)将同步synchronized替换为显式的Lock操作;


(2)将Object类中的wait(), notify(),notifyAll()替换成了Condition对象,该对象可以通过Lock锁对象获取;


(3)一个Lock对象上可以绑定多个Condition对象,这样实现了本方线程只唤醒对方线程,而jdk1.5之前,一个同步只能有一个锁,不同的同步只能用锁来区分,且锁嵌套时容易死锁。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
csharp复制代码    class Resource{  
private String name;
private int count=1;
private boolean flag=false;
private Lock lock = new ReentrantLock();/*Lock是一个接口,ReentrantLock是该接口的一个直接子类。*/
private Condition condition_pro=lock.newCondition(); /*创建代表生产者方面的Condition对象*/
private Condition condition_con=lock.newCondition(); /*使用同一个锁,创建代表消费者方面的Condition对象*/

public void set(String name){
lock.lock();//锁住此语句与lock.unlock()之间的代码
try{
while(flag)
condition_pro.await(); //生产者线程在conndition_pro对象上等待
this.name=name+"---"+count++;
System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
flag=true;
condition_con.signalAll();
}
finally{
lock.unlock(); //unlock()要放在finally块中。
}
}
public void out(){
lock.lock(); //锁住此语句与lock.unlock()之间的代码
try{
while(!flag)
condition_con.await(); //消费者线程在conndition_con对象上等待
System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);
flag=false;
condition_pro.signqlAll(); /*唤醒所有在condition_pro对象下等待的线程,也就是唤醒所有生产者线程*/
}
finally{
lock.unlock();
}
}
}

3、使用阻塞队列(BlockingQueue)控制线程通信

BlockingQueue是一个接口,也是Queue的子接口。\*\*BlockingQueue具有一个特征:\*\*当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果队列已空,则该线程阻塞。程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。

BlockingQueue提供如下两个支持阻塞的方法:

(1)put(E e):尝试把Eu元素放入BlockingQueue中,如果该队列的元素已满,则阻塞该线程。

(2)take():尝试从BlockingQueue的头部取出元素,如果该队列的元素已空,则阻塞该线程。

BlockingQueue继承了Queue接口,当然也可以使用Queue接口中的方法,这些方法归纳起来可以分为如下三组:

(1)在队列尾部插入元素,包括add(E e)、offer(E e)、put(E e)方法,当该队列已满时,这三个方法分别会抛出异常、返回false、阻塞队列。

(2)在队列头部删除并返回删除的元素。包括remove()、poll()、和take()方法,当该队列已空时,这三个方法分别会抛出异常、返回false、阻塞队列。

(3)在队列头部取出但不删除元素。包括element()和peek()方法,当队列已空时,这两个方法分别抛出异常、返回false。

BlockingQueue接口包含如下5个实现类:

1
2
3
4
5
6
7
8
9
10
11
arduino复制代码ArrayBlockingQueue :基于数组实现的BlockingQueue队列。

LinkedBlockingQueue:基于链表实现的BlockingQueue队列。

PriorityBlockingQueue:它并不是保准的阻塞队列,该队列调用remove()、poll()、take()等方法提取出元素时,并不是取出队列中存在时间最长的元素,而是队列中最小的元素。
它判断元素的大小即可根据元素(实现Comparable接口)的本身大小来自然排序,也可使用Comparator进行定制排序。

SynchronousQueue:同步队列。对该队列的存、取操作必须交替进行。

DelayQueue:它是一个特殊的BlockingQueue,底层基于PriorityBlockingQueue实现,不过,DelayQueue要求集合元素都实现Delay接口(该接口里只有一个long getDelay()方法),
DelayQueue根据集合元素的getDalay()方法的返回值进行排序。

copy的一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
java复制代码 1 import java.util.concurrent.ArrayBlockingQueue;
2 import java.util.concurrent.BlockingQueue;
3 public class BlockingQueueTest{
4 public static void main(String[] args)throws Exception{
5 //创建一个容量为1的BlockingQueue
6
7 BlockingQueue<String> b=new ArrayBlockingQueue<>(1);
8 //启动3个生产者线程
9 new Producer(b).start();
10 new Producer(b).start();
11 new Producer(b).start();
12 //启动一个消费者线程
13 new Consumer(b).start();
14
15 }
16 }
17 class Producer extends Thread{
18 private BlockingQueue<String> b;
19
20 public Producer(BlockingQueue<String> b){
21 this.b=b;
22
23 }
24 public synchronized void run(){
25 String [] str=new String[]{
26 "java",
27 "struts",
28 "Spring"
29 };
30 for(int i=0;i<9999999;i++){
31 System.out.println(getName()+"生产者准备生产集合元素!");
32 try{
33
34 b.put(str[i%3]);
35 sleep(1000);
36 //尝试放入元素,如果队列已满,则线程被阻塞
37
38 }catch(Exception e){System.out.println(e);}
39 System.out.println(getName()+"生产完成:"+b);
40 }
41
42 }
43 }
44 class Consumer extends Thread{
45 private BlockingQueue<String> b;
46 public Consumer(BlockingQueue<String> b){
47 this.b=b;
48 }
49 public synchronized void run(){
50
51 while(true){
52 System.out.println(getName()+"消费者准备消费集合元素!");
53 try{
54 sleep(1000);
55 //尝试取出元素,如果队列已空,则线程被阻塞
56 b.take();
57 }catch(Exception e){System.out.println(e);}
58 System.out.println(getName()+"消费完:"+b);
59 }
60
61 }
62 }

六、线程池

合理利用线程池能够带来三个好处。

  1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  2. 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

1、使用Executors工厂类产生线程池

Executor线程池框架的最大优点是把任务的提交和执行解耦。客户端将要执行的任务封装成Task,然后提交即可。而Task如何执行客户端则是透明的。具体点讲,提交一个Callable对象给ExecutorService(如最常用的线程池ThreadPoolExecutor),将得到一个Future对象,调用Future对象的get方法等待执行结果。线程池实现原理类结构图如下:

上图中涉及到的线程池内部实现原理的所有类,不利于我们理解线程池如何使用。我们先从客户端的角度出发,看看客户端使用线程池所涉及到的类结构图:

由上图可知,ExecutorService是Java中对线程池定义的一个接口,它java.util.concurrent包中。Java API对ExecutorService接口的实现有两个,所以这两个即是Java线程池具体实现类如下:

1
2
复制代码 ThreadPoolExecutor
ScheduledThreadPoolExecutor
除此之外,ExecutorService还继承了Executor接口(注意区分Executor接口和Executors工厂类),这个接口只有一个execute()方法,最后我们看一下整个继承树:


使用Executors执行多线程任务的步骤如下:

• 调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池;

• 创建Runnable实现类或Callable实现类的实例,作为线程执行任务;

• 调用ExecutorService对象的submit()方法来提交Runnable实例或Callable实例;

• 当不想提交任务时,调用ExecutorService对象的shutdown()方法来关闭线程池。

(1)使用Executors的静态工厂类创建线程池的方法如下:

1、newFixedThreadPool() :

作用:该方法返回一个固定线程数量的线程池,该线程池中的线程数量始终不变,即不会再创建新的线程,也不会销毁已经创建好的线程,自始自终都是那几个固定的线程在工作,所以该线程池可以控制线程的最大并发数。
栗子:假如有一个新任务提交时,线程池中如果有空闲的线程则立即使用空闲线程来处理任务,如果没有,则会把这个新任务存在一个任务队列中,一旦有线程空闲了,则按FIFO方式处理任务队列中的任务。

2、newCachedThreadPool() :

作用:该方法返回一个可以根据实际情况调整线程池中线程的数量的线程池。即该线程池中的线程数量不确定,是根据实际情况动态调整的。

栗子:假如该线程池中的所有线程都正在工作,而此时有新任务提交,那么将会创建新的线程去处理该任务,而此时假如之前有一些线程完成了任务,现在又有新任务提交,那么将不会创建新线程去处理,而是复用空闲的线程去处理新任务。那么此时有人有疑问了,那这样来说该线程池的线程岂不是会越集越多?其实并不会,因为线程池中的线程都有一个“保持活动时间”的参数,通过配置它,如果线程池中的空闲线程的空闲时间超过该“保存活动时间”则立刻停止该线程,而该线程池默认的“保持活动时间”为60s。

3、newSingleThreadExecutor() :

作用:该方法返回一个只有一个线程的线程池,即每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待这一个线程空闲,当这个线程空闲了再按FIFO方式顺序执行任务队列中的任务。

4、newScheduledThreadPool() :

作用:该方法返回一个可以控制线程池内线程定时或周期性执行某任务的线程池。

5、newSingleThreadScheduledExecutor() :

作用:该方法返回一个可以控制线程池内线程定时或周期性执行某任务的线程池。只不过和上面的区别是该线程池大小为1,而上面的可以指定线程池的大小。

注:Executors只是一个工厂类,它所有的方法返回的都是ThreadPoolExecutor、ScheduledThreadPoolExecutor这两个类的实例。

(2) ExecutorService有如下几个执行方法:

1
2
3
4
5
scss复制代码- execute(Runnable)
- submit(Runnable)
- submit(Callable)
- invokeAny(...)
- invokeAll(...)

execute(Runnable)

这个方法接收一个Runnable实例,并且异步的执行,请看下面的实例:

1
2
3
4
5
6
7
8
9
csharp复制代码ExecutorService executorService = Executors.newSingleThreadExecutor();

executorService.execute(new Runnable() {
public void run() {
System.out.println("Asynchronous task");
}
});

executorService.shutdown();

submit(Runnable)

submit(Runnable)和execute(Runnable)区别是前者可以返回一个Future对象,通过返回的Future对象,我们可以检查提交的任务是否执行完毕,请看下面执行的例子:

1
2
3
4
5
6
7
csharp复制代码Future future = executorService.submit(new Runnable() {
public void run() {
System.out.println("Asynchronous task");
}
});

future.get(); //returns null if the task has finished correctly.

注:如果任务执行完成,future.get()方法会返回一个null。注意,future.get()方法会产生阻塞。

submit(Callable)

submit(Callable)和submit(Runnable)类似,也会返回一个Future对象,但是除此之外,submit(Callable)接收的是一个Callable的实现,Callable接口中的call()方法有一个返回值,可以返回任务的执行结果,而Runnable接口中的run()方法是void的,没有返回值。请看下面实例:

1
2
3
4
5
6
7
8
csharp复制代码Future future = executorService.submit(new Callable(){
public Object call() throws Exception {
System.out.println("Asynchronous Callable");
return "Callable Result";
}
});

System.out.println("future.get() = " + future.get());

如果任务执行完成,future.get()方法会返回Callable任务的执行结果。另外,future.get()方法会产生阻塞。

invokeAny(…)

invokeAny(…)方法接收的是一个Callable的集合,执行这个方法不会返回Future,但是会返回所有Callable任务中其中一个任务的执行结果。这个方法也无法保证返回的是哪个任务的执行结果,反正是其中的某一个。请看下面实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
vbnet复制代码ExecutorService executorService = Executors.newSingleThreadExecutor();

Set<Callable<String>> callables = new HashSet<Callable<String>>();

callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 1";
}
});
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 2";
}
});
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 3";
}
});
String result = executorService.invokeAny(callables);
System.out.println("result = " + result);
executorService.shutdown();

大家可以尝试执行上面代码,每次执行都会返回一个结果,并且返回的结果是变化的,可能会返回“Task2”也可是“Task1”或者其它。

invokeAll(…)

invokeAll(…)与 invokeAny(…)类似也是接收一个Callable集合,但是前者执行之后会返回一个Future的List,其中对应着每个Callable任务执行后的Future对象。情况下面这个实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
vbnet复制代码ExecutorService executorService = Executors.newSingleThreadExecutor();

Set<Callable<String>> callables = new HashSet<Callable<String>>();

callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 1";
}
});
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 2";
}
});
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 3";
}
});

List<Future<String>> futures = executorService.invokeAll(callables);
for(Future<String> future : futures){
System.out.println("future.get = " + future.get());
}
executorService.shutdown();

(3) ExecutorService关闭方法

当我们使用完成ExecutorService之后应该关闭它,否则它里面的线程会一直处于运行状态。举个例子,如果的应用程序是通过main()方法启动的,在这个main()退出之后,如果应用程序中的ExecutorService没有关闭,这个应用将一直运行。之所以会出现这种情况,是因为ExecutorService中运行的线程会阻止JVM关闭。

要关闭ExecutorService中执行的线程,我们可以调用ExecutorService.shutdown()方法。在调用shutdown()方法之后,ExecutorService不会立即关闭,但是它不再接收新的任务,直到当前所有线程执行完成才会关闭,所有在shutdown()执行之前提交的任务都会被执行。

如果想立即关闭ExecutorService,我们可以调用ExecutorService.shutdownNow()方法。这个动作将跳过所有正在执行的任务和被提交还没有执行的任务。但是它并不对正在执行的任务做任何保证,有可能它们都会停止,也有可能执行完成。

2、使用Java8增强的ForkJoinPool产生线程池

在Java 8中,引入了自动并行化的概念。它能够让一部分Java代码自动地以并行的方式执行,前提是使用了ForkJoinPool。

ForkJoinPool同ThreadPoolExecutor一样,也实现了Executor和ExecutorService接口。它使用了一个无限队列来保存需要执行的任务,而线程的数量则是通过构造函数传入,如果没有向构造函数中传入希望的线程数量,那么当前计算机可用的CPU数量会被设置为线程数量作为默认值。

ForkJoinPool主要用来使用分治法(Divide-and-Conquer Algorithm)来解决问题。典型的应用比如快速排序算法。这里的要点在于,ForkJoinPool需要使用相对少的线程来处理大量的任务。比如要对1000万个数据进行排序,那么会将这个任务分割成两个500万的排序任务和一个针对这两组500万数据的合并任务。以此类推,对于500万的数据也会做出同样的分割处理,到最后会设置一个阈值来规定当数据规模到多少时,停止这样的分割处理。比如,当元素的数量小于10时,会停止分割,转而使用插入排序对它们进行排序。那么到最后,所有的任务加起来会有大概2000000+个。问题的关键在于,对于一个任务而言,只有当它所有的子任务完成之后,它才能够被执行。所以当使用ThreadPoolExecutor时,使用分治法会存在问题,因为ThreadPoolExecutor中的线程无法像任务队列中再添加一个任务并且在等待该任务完成之后再继续执行。而使用ForkJoinPool时,就能够让其中的线程创建新的任务,并挂起当前的任务,此时线程就能够从队列中选择子任务执行。比如,我们需要统计一个double数组中小于0.5的元素的个数,那么可以使用ForkJoinPool进行实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
ini复制代码public class ForkJoinTest {
private double[] d;
private class ForkJoinTask extends RecursiveTask {
private int first;
private int last;
public ForkJoinTask(int first, int last) {
this.first = first;
this.last = last;
}
protected Integer compute() {
int subCount;
if (last - first < 10) {
subCount = 0;
for (int i = first; i <= last; i++) {
if (d[i] < 0.5){
subCount++;
}
}
}else {
int mid = (first + last) /2;
ForkJoinTask left = new ForkJoinTask(first, mid);
left.fork();
ForkJoinTask right = new ForkJoinTask(mid + 1, last);
right.fork();
subCount = left.join();
subCount += right.join();
}
return subCount;
}
}
public static void main(String[] args) {
ForkJoinPool pool=new ForkJoinPool();
pool.submit(new ForkJoinTask(0, 9999999));
pool.awaitTermination(2,TimeUnit.SECONDS);
System.out.println("Found " + n + " values");
}
}

以上的关键是fork()和join()方法。在ForkJoinPool使用的线程中,会使用一个内部队列来对需要执行的任务以及子任务进行操作来保证它们的执行顺序。

注:使用ThreadPoolExecutor和ForkJoinPool的性能差异:

(1)首先,使用ForkJoinPool能够使用数量有限的线程来完成非常多的具有父子关系的任务,比如使用4个线程来完成超过200万个任务。但是,使用ThreadPoolExecutor时,是不可能完成的,因为ThreadPoolExecutor中的Thread无法选择优先执行子任务,需要完成200万个具有父子关系的任务时,也需要200万个线程,显然这是不可行的。

(2)ForkJoinPool能够实现工作窃取(Work Stealing),在该线程池的每个线程中会维护一个队列来存放需要被执行的任务。当线程自身队列中的任务都执行完毕后,它会从别的线程中拿到未被执行的任务并帮助它执行。因此,提高了线程的利用率,从而提高了整体性能。

(3)对于ForkJoinPool,还有一个因素会影响它的性能,就是停止进行任务分割的那个阈值。比如在之前的快速排序中,当剩下的元素数量小于10的时候,就会停止子任务的创建。

结论:

  1. 当需要处理递归分治算法时,考虑使用ForkJoinPool;
  2. 仔细设置不再进行任务划分的阈值,这个阈值对性能有影响;
  3. Java 8中的一些特性会使用到ForkJoinPool中的通用线程池。在某些场合下,需要调整该线程池的默认的线程数量。

七、死锁

产生死锁的四个必要条件如下。当下边的四个条件都满足时即产生死锁,即任意一个条件不满足既不会产生死锁。

(1)死锁的四个必要条件

  • 互斥条件:资源不能被共享,只能被同一个进程使用
  • 请求与保持条件:已经得到资源的进程可以申请新的资源
  • 非剥夺条件:已经分配的资源不能从相应的进程中被强制剥夺
  • 循环等待条件:系统中若干进程组成环路,该环路中每个进程都在等待相邻进程占用的资源
举个常见的死锁例子:进程A中包含资源A,进程B中包含资源B,A的下一步需要资源B,B的下一步需要资源A,所以它们就互相等待对方占有的资源释放,所以也就产生了一个循环等待死锁。

(2)处理死锁的方法

  • 忽略该问题,也即鸵鸟算法。当发生了什么问题时,不管他,直接跳过,无视它;
  • 检测死锁并恢复;
  • 资源进行动态分配;
  • 破除上面的四种死锁条件之一。

八、线程相关类

(1)ThreadLocal

 ThreadLocal它并不是一个线程,而是一个可以在每个线程中存储数据的数据存储类,通过它可以在指定的线程中存储数据,数据存储之后,只有在指定线程中可以获取到存储的数据,对于其他线程来说则无法获取到该线程的数据。 即多个线程通过同一个ThreadLocal获取到的东西是不一样的,就算有的时候出现的结果是一样的(偶然性,两个线程里分别存了两份相同的东西),但他们获取的本质是不同的。使用这个工具类可以简化多线程编程时的并发访问,很简洁的隔离多线程程序的竞争资源。


对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。ThreadLocal类提供了如下的三个public方法:
1
2
3
4
5
6
scss复制代码ThreadLocal()
创建一个线程本地变量。
T get()
返回此线程局部变量的当前线程副本中的值,如果这是线程第一次调用该方法,则创建并初始化此副本。
protected T initialValue()
返回此线程局部变量的当前线程的初始值。
下面通过系统源码来分析出现这个结果的原因。 在ThreadLocal中存在着两个很重要的方法,get()和set()方法,一个读取一个设置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
scss复制代码    /**
* Returns the value of this variable for the current thread. If an entry
* doesn't yet exist for this variable on this thread, this method will
* create an entry, populating the value with the result of
* {@link #initialValue()}.
*
* @return the current value of the variable for the calling thread.
*/
@SuppressWarnings("unchecked")
public T get() {
// Optimized for the fast path.
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);
if (values != null) {
Object[] table = values.table;
int index = hash & values.mask;
if (this.reference == table[index]) {
      return (T) table[index + 1];
}
} else {
    values = initializeValues(currentThread);
  }
  return (T) values.getAfterMiss(this);
}
/**
* Sets the value of this variable for the current thread. If set to
* {@code null}, the value will be set to null and the underlying entry will
* still be present.
*
* @param value the new value of the variable for the caller thread.
*/
public void set(T value) {
Thread currentThread = Thread.currentThread();
   Values values = values(currentThread);
   if (values == null) {
     values = initializeValues(currentThread);
   }
values.put(this, value);
}

从注释上可以看出,get方法会返回一个当前线程的变量值,如果数组不存在就会创建一个新的。另外,对于“当前线程”和“数组”,数组对于每个线程来说都是不同的 values.table。而values是通过当前线程获取到的一个Values对象,因此这个数组是每个线程唯一的,不能共用,而下面的几句话也更直接了,获取一个索引,再返回通过这个索引找到数组中对应的值。这也就解释了为什么多个线程通过同一个ThreadLocal返回的是不同的东西。

Java中为什么要这么设置呢?

  • ThreadLocal在日常开发中使用到的地方较少,但是在某些特殊的场景下,通过ThreadLocal可以轻松实现一些看起来很复杂的功能。一般来说,当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑使用ThreadLocal。例如在Handler和Looper中。对于Handler来说,它需要获取当前线程的Looper,很显然Looper的作用域就是线程并且不同的线程具有不同的Looper,这个时候通过ThreadLocal就可以轻松的实现Looper在线程中的存取。如果不采用ThreadLocal,那么系统就必须提供一个全局的哈希表供Handler查找指定的Looper,这样就比较麻烦了,还需要一个管理类。
  • ThreadLocal的另一个使用场景是复杂逻辑下的对象传递,比如监听器的传递,有些时候一个线程中的任务过于复杂,就可能表现为函数调用栈比较深以及代码入口的多样性,这种情况下,我们又需要监听器能够贯穿整个线程的执行过程。这个时候就可以使用到ThreadLocal,通过ThreadLocal可以让监听器作为线程内的全局对象存在,在线程内通过get方法就可以获取到监听器。如果不采用的话,可以使用参数传递,但是这种方式在设计上不是特别好,当调用栈很深的时候,通过参数来传递监听器这个设计太糟糕。而另外一种方式就是使用static静态变量的方式,但是这种方式存在一定的局限性,拓展性并不是特别的强。比如有10个线程在执行,就需要提供10个监听器对象。

注:ThreadLocal和其他所有的同步机制一样,都是为了解决多线程中对于同一变量的访问冲突。在普通的同步机制中,通过对象加锁来实现多线程对同一变量的安全访问,且该变量是多线程共享的,所有需要使用这种同步机制来明确分开是在什么时候对变量进行读写,在什么时候需要锁定该对象。此种情况下,系统并没有将这个资源复制多份,而是采取安全机制来控制访问而已。ThreadLocal只是从另一个角度解决多线程的并发访问,即将需要并发访问的资源复制多份,每个线程拥有一份资源,每个线程都有自己的资源副本。

总结:若多个线程之间需要共享资源,以达到线程间的通信时,就使用同步机制;若仅仅需要隔离多线程之间的关系资源,则可以使用ThreadLocal。

原文链接:www.cnblogs.com/snow-flower…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

前任发来这个:(x^2+y^2-1)^3+x^2*y^3

发表于 2021-06-04

原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处。

正要下班,前任发来一串代码。按照一贯的尿性来看,这应该是TMD发错了。但鉴于今天日期特殊,仔细思考了一下好像不太对。

在线markdown脑图直接看,访问 mind.xjjdog.cn 即可获取

image-20210520173531594.png

作为程序员,肯定是要解一下码的。就像看到库子,就想要把它拖下来一样。

把公式代入Java代码一看,了不得了不得。

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class X520 {
public static void main(String[] args) {
for (float y = 1.5f; y >= -1.5f; y -= 0.1f) {
for (float x = -1.5f; x <= 1.5f; x += 0.05f) {
float a = x * x + y * y - 1;
System.out.print(a * a * a - x * x * y * y * y <= 0.0f ? "$" : " ");
}
System.out.println();
}
}
}

竟然输出了一个可以让死灰复燃的图形。有了这个图形,我觉得以后可以被两团热情的火焰烘烤了。

image-20210520173251378.png

为何这段小公式有这样的魔力?我们要在wolframalpha上看一下。

wolframalpha是一个卓越的在线积分计算器。这里的积分,不是说的腾讯QQ这样的积分,而是真正的微积分。可用来计算反导数和定积分、双重和三重积分以及反常积分。

我们把公式输到里面,得出的图形竟然更加的绚丽灿烂,让人拍案惊奇。

image-20210520173945827.png

研究了一番,我发现了另外一个更加平滑的心形公式,更加的美丽动人。就是下面这个代码:

1
bash复制代码polar plot r = 2 - 2 sin theta + sin theta * ( sqrt( |cos theta| ) ) / ( sin theta + 1.4 )

它产出的图形是这样的!

image-20210520174257231.png
看这小心尖,像猫爪子一样挠人,让人心痒痒。

既然这个工具能够写代码,那我也要自己卓越的头脑造一个专有的图形。但是想了半天,只想到了这个…

1
ini复制代码1+1 = 2

image-20210520174511883.png

公式写得出来写不出来,这都是次要的。

最重要的是,今晚要加班,但是明天会请假。

说话之间,又找到一段这样的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
java复制代码public class X520 {
public static void main(String[] args) {
double a, b, n = 10;
String message = " I'm fucking love Xjjdog ";

int print_message = 4;
if (message.length() % 2 != 0) {
message += " ";
}
for (a = 0; a < n; a++) {
for (b = 0; b <= 4 * n; b++) {
double distance1 = Math.sqrt(Math.pow(a - n, 2) + Math.pow(b - n, 2));
double distance2 = Math.sqrt(Math.pow(a - n, 2) + Math.pow(b - 3 * n, 2));
if (distance1 < n + 0.5 || distance2 < n + 0.5) {
System.out.print("S");
} else {
System.out.print(" ");
}
}
System.out.println();
}
for (a = 1; a < 2 * n; a++) {
for (b = 0; b < a; b++)
System.out.print(" ");
for (b = 0; b < 4 * n + 1 - 2 * a; b++) {
if (a >= print_message - 1 && a <= print_message + 1) {
double point = b - (4 * n - 2 * a - message.length()) / 2;

// prints message after leaving
// appropriate space
if (point < message.length() &&
point >= 0) {
if (a == print_message)
System.out.print
(message.charAt((int)point));
else
System.out.print(" ");
}

else
System.out.print("S");
}

else
System.out.print("S");
}

System.out.println();
}
}
}

// This code is contributed by Anant Agarwal.

作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,进一步交流。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

我们并不需要 Deno

发表于 2021-06-04

作者:LeanCloud 王子亭

Deno 一出生便带着光环 —— 它发布于 Node.js 创始人 Ryan Dahl 的演讲「Design Mistakes in Node(幻灯片)」,当时有些人说 Node.js 要凉了,但我不这么认为。

原生 TypeScript

其实目前我们在引擎的「用户态」去使用 TypeScript 并没有引入任何问题,而且给用户带来了很大的灵活性。考虑到 TypeScript 不可能离开 JavaScript 的生态 —— 毕竟引擎总是要支持 JavaScript 的;再加上 TypeScript 有不同的版本、不同的编译开关,在用户态使用 TypeScript 可以说是最好的方案了。TypeScirpt 迟早会成为 Deno 的历史包袱。

从性能的角度,在 TypeScript 没出现之前,V8 已经在 JavaScript 上进行大量 魔法优化 了,可以说 JIT 出来的代码并不比其他静态类型的语言差太多,是没法简单地通过 TypeScript 来提升性能的。再加上前面说了引擎总还是要支持 JavaScript、TypeScript 的运行时语义依然是 JavaScript(TypeScript 并不能保证对象的实际类型在运行时不被修改),所以引擎也不可能从对 JavaScript 的魔法优化切换到基于 TypeScript 的类型来做优化。

包管理器

我一直认为 NPM 是最好用的包管理器之一,这包括将依赖保存在项目目录中 —— 在调整一个项目的依赖时不必担心对其他项目产生影响;每个包都可以指定自己的依赖版本,允许多版本并存 —— 在升级一个包的依赖时不会影响到其他包,每个包都可以使用新的版本或继续使用旧的版本;NPM 负责查找和安装包,而 Node.js 则用相对简单的协议去使用这些包,它们可以彼此独立地升级演进。

可以看到 NPM 最终极大地减轻了开发者的心智负担,只要你按照正确的方式去使用它,极少会遇到其他语言中有关依赖管理的问题。而 Deno 则反其道行之。虽然 Deno 也提供了一些相关的功能(deno cache),但你会发现 Deno 的本意仍然是不希望进行「依赖管理」。

在代码中包含 URL 是一个非常糟糕的做法(Golang 也是如此),Deno 称之为去中心化,但其实它只是重新将使用包的代码与包的来源耦合在了一起(现在 Deno 提供了一个 官方的代理,但这样和 NPM 的中心仓库又有什么区别呢)。缓存机制也带来了相当大的不确定性:package-lock.json 可以保证每次安装的依赖是完全一致的,而 Deno 的 lock.json 只能检查依赖是否有变化(如果有的话就拒绝运行)。这使得开发者很难控制依赖更新的时机,Deno 则建议将依赖缓存放入 Git。

内建权限系统

一直以来通用编程语言都不曾在语言层面引入权限控制,但确实开源社区也曾报出过多次恶意代码的事件,但 Deno 的权限机制相当粗糙 —— 只能在进程级别进行权限控制,我可以大胆地预言,在几乎所有的场景里我们都需要 –allow-all,并不能对安全起到太多作用。

我们需要考虑 Deno 的用户到底是开发者还是使用者:对于 Deno 脚本的使用者来说关注的当然是进程级别的权限;而对于开发者我认为更关注的是第三方包的权限,权限系统应该以包为单位(然而 Deno 里并没有包的概念了),Node 里本来也有 vm 模块可以一定程度上实现沙盒(但确实非常难以控制)。

而且说起来我们现在已经有了 Docker(或者更广泛的容器的概念)这种彻底的隔离和权限控制机制,业界对编程语言引入一套权限控制已经没有太大的需求了。

孤立的生态

可以说 JavaScript 的生态来自于用户态类库的充分竞争,Deno 则在 Runtime API 之外提供了 Standard Library(类似 golang.org/x)、提供了全套的开发工具链(fmt、test、doc、lint、bundle),在试图提供开箱即用的使用体验的同时,也削弱了第三方生态。

在 Node.js 和 NPM 已然成为 JavaScript 事实标准的一部分的情况下,Deno 本来可以通过兼容 Node.js 或 NPM 有一个非常好的开场。但 Deno 却选择了和 Node.js 划清界限,而是兼容了一些浏览器环境的 API(如 prompt 或 onload)。

Deno 自己的说法是为了遵循已有的 Web 标准避免发明新东西,但实际上这些 Web 标准在设计时并未充分考虑浏览器之外的 Runtime,况且 Deno 其实也没能避免发明新东西(这些新东西被放在了 Deno 这个命名空间中)。

小结

Deno 就是这样一个有着非常鲜明个人偏好的 JavaScript Runtime,它试图去纠正 Node.js 的一些「设计失误」、希望给出一种「JavaScript 最佳实践」,希望提供高质量且开箱即用的标准库和工具链。这些偏好的选择总会有人喜欢或不喜欢,但除此之外 Deno 实在是缺少一个 killer feature(杀手级特性)让一个「理性」的 Node.js 开发者(如一个公司)切换到 Deno。

通过单一文件发行、进程级别的权限控制使 Deno 会更适合命令行工具的开发,但能否与已经广泛用于命令行工具的 Golang 竞争尚且存疑。

作为一个 Node.js 开发者,我并不觉得 Deno 可以在未来替代 Node 成为我的主力开发工具,Deno 更像是 Golang 的设计哲学对 JavaScript 的一次入侵。

原文地址:我们并不需要 Deno

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

删库跑路如何预防?— Oracle创建只读账号详细教程

发表于 2021-06-04

需求说明

现有数据库账号:HEPSUSR:具有完整权限,增删改查。

需要创建一个数据库账号:HTREADER,对HEPSUSR账号下所有的表具有只读权限。

第一步:创建只读账号

1
2
sql复制代码--创建只读账号 第一步
CREATE USER htreader identified by 123456;

第二步:赋予账号连接数据库等基本权限

1
2
3
4
5
sql复制代码--赋予htreader连接等常规权限
grant connect to htreader;
grant create view to htreader;
grant create session to htreader;
grant create synonym to htreader;

第三步:获取原账号的查询权限

1
2
3
4
5
6
7
csharp复制代码获取原账号HEPSUSR用户的所有查询表权限
select 'grant select on '||owner||'.'||object_name||' to htreader;'
from dba_objects
where owner in ('HEPSUSR')
and object_type='TABLE';

--查询结果为新账号的赋值语句,如下图

“)​)​​

​

第四步:将原账号权限赋值为新账号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
vbnet复制代码在原账号HEPSUSR下执行,将原账号的查询权限 赋值给新账号
-------
grant select on HEPSUSR.ENTRY_CERT to htreader;
grant select on HEPSUSR.SUB_MESSAGE_INFO to htreader;
grant select on HEPSUSR.ENTRY_CERT_RELATION to htreader;
grant select on HEPSUSR.ENTRY_CERT_RELATION to htreader;
grant select on HEPSUSR.ENTRY_DECL_TAX to htreader;
grant select on HEPSUSR.ENTRY_DOCU to htreader;
grant select on HEPSUSR.ENTRY_FEES to htreader;
grant select on HEPSUSR.ENTRY_GOODS_TAX to htreader;
grant select on HEPSUSR.ENTRY_HEAD to htreader;
grant select on HEPSUSR.ENTRY_LIST to htreader;
grant select on HEPSUSR.ENTRY_WORKFLOW to htreader;
grant select on HEPSUSR.IQ_APPEND to htreader;
grant select on HEPSUSR.IQ_CERT to htreader;
grant select on HEPSUSR.SUB_SWAP to htreader;
grant select on HEPSUSR.VIN_LIST to htreader;

第五步:在新账号端创建同位显示表

因为新创建的只读账号,Tables栏中显示为空,我们需要在PL/SQL显示栏中为新账号登录界面添加显示同位元素,如下:

1
2
3
4
5
csharp复制代码--在原账号HEPSUSR端执行,获取需要显示的表名称
select 'create or replace SYNONYM htreader.'||object_name|| ' for ' ||owner|| '.'||object_name||';'
from dba_objects
where owner in ('HEPSUSR')
and object_type='TABLE'

“)​)​​

​

第六步:查询结果在新账号端执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ini复制代码在只读账号HTREADER端执行:添加显示各个表信息;在SYSNONYM目录下,tables目录下无显示
create or replace SYNONYM htreader.VIN_LIST for HEPSUSR.VIN_LIST;
create or replace SYNONYM htreader.SUB_SWAP for HEPSUSR.SUB_SWAP;
create or replace SYNONYM htreader.SUB_MESSAGE_INFO for HEPSUSR.SUB_MESSAGE_INFO;
create or replace SYNONYM htreader.IQ_CERT for HEPSUSR.IQ_CERT;
create or replace SYNONYM htreader.IQ_APPEND for HEPSUSR.IQ_APPEND;
create or replace SYNONYM htreader.ENTRY_WORKFLOW for HEPSUSR.ENTRY_WORKFLOW;
create or replace SYNONYM htreader.ENTRY_LIST for HEPSUSR.ENTRY_LIST;
create or replace SYNONYM htreader.ENTRY_HEAD for HEPSUSR.ENTRY_HEAD;
create or replace SYNONYM htreader.ENTRY_GOODS_TAX for HEPSUSR.ENTRY_GOODS_TAX;
create or replace SYNONYM htreader.ENTRY_FEES for HEPSUSR.ENTRY_FEES;
create or replace SYNONYM htreader.ENTRY_DOCU for HEPSUSR.ENTRY_DOCU;
create or replace SYNONYM htreader.ENTRY_DECL_TAX for HEPSUSR.ENTRY_DECL_TAX;
create or replace SYNONYM htreader.ENTRY_CONTAINER for HEPSUSR.ENTRY_CONTAINER;
create or replace SYNONYM htreader.ENTRY_CERT_RELATION for HEPSUSR.ENTRY_CERT_RELATION;
create or replace SYNONYM htreader.ENTRY_CERT for HEPSUSR.ENTRY_CERT;

第七步:执行完成之后 登录新账号,查看结果

新账号可以查询原账号的所有表结构,但是无法执行 增删改相关操作

)​

第八步:执行删除、修改sql语句测试

)​

附录:Oracle查询账号及权限详细语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
sql复制代码1.查看所有用户:
select * from dba_users;
select * from all_users;
select * from user_users;

2.查看用户或角色系统权限(直接赋值给用户或角色的系统权限):
select * from dba_sys_privs;
select * from user_sys_privs;

3.查看角色(只能查看登陆用户拥有的角色)所包含的权限
sql>select * from role_sys_privs;

4.查看用户对象权限:
select * from dba_tab_privs;
select * from all_tab_privs;
select * from user_tab_privs;

5.查看所有角色:
select * from dba_roles;

6.查看用户或角色所拥有的角色:
select * from dba_role_privs;
select * from user_role_privs;

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

sublime text 3 打造python3环境(代码自

发表于 2021-06-04

最近用python3用的比较多,于是整理一下环境,此博客仅记录下自己构造python3环境的过程(sublime text 3版本为3176):

安装的一些常用插件和配置过程如下:
sublime 下载地址,插件安装方法参考另一篇博客
blog.csdn.net/zxy98787267…

  • TrailingSpaces高亮显示多余的空格和Tab
  • BracketHighlighter高亮显示匹配的括号、引号和标签
  • ChineseLocalizations各国语言包
  • SublimeREPL 可以用于运行和调试一些需要交互的程序(可以直接运行当前程序,或者使用ipython、pdb调试代码)

SublimeREPL 安装完成以后,可以通过Tools—SublimeREPL—Python,运行当前的程序;
使用ctrl+b运行程序,步骤如下:
更改配置前先要设置python编译系统,Tools->Build System->New build Systems,将内容改为以下内容

1
2
3
4
5
6
swift复制代码{
"cmd": ["C:/Users/AppData/Local/Programs/Python/Python36-32/python.exe","-u","$file"],
"file_regex": "^[ ]*File \"(...*?)\", line ([0-9]*)",
"selector": "source.python",
"encoding": "cp936",
}

python.exe为自己电脑上python的路径,encoding的查看方法为:win+R,进入cmd,点击属性进入如下界面:
这里写图片描述

将文件命名为python3.sublime-build,保存退出!
进入Tools->Build System,选择刚才命名的python3,配置完成后,此时ctrl+b可以运行当前程序。

也可以根据习惯配置运行程序的快捷键,打开Preferences->Browse Packages->SublimeREPL->config->Python->Default.sublime-commands,文件如下:

1
css复制代码[    {        "caption": "SublimeREPL: Python",        "command": "run_existing_window_command", "args":        {            "id": "repl_python",            "file": "config/Python/Main.sublime-menu"        }    },    {        "caption": "SublimeREPL: Python - PDB current file",        "command": "run_existing_window_command", "args":        {            "id": "repl_python_pdb",            "file": "config/Python/Main.sublime-menu"        }    },    {        "caption": "SublimeREPL: Python - RUN current file",        "command": "run_existing_window_command", "args":        {            "id": "repl_python_run",            "file": "config/Python/Main.sublime-menu"        }    },    {        "command": "python_virtualenv_repl",        "caption": "SublimeREPL: Python - virtualenv"    },    {        "caption": "SublimeREPL: Python - IPython",        "command": "run_existing_window_command", "args":        {            "id": "repl_python_ipython",            "file": "config/Python/Main.sublime-menu"        }    }]

将此处配置复制到Preferences->key bindings,定义自己的快捷键,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
css复制代码[{ "keys": ["f5"], "caption": "SublimeREPL: Python - RUN current file",
"command": "run_existing_window_command", "args":
{
"id": "repl_python_run",
"file": "config/Python/Main.sublime-menu"
}
},
{ "keys": ["f1"], "caption": "SublimeREPL: Python",
"command": "run_existing_window_command", "args":
{
"id": "repl_python",
"file": "config/Python/Main.sublime-menu"
}
}
]

配置完成后,保存即可,若需要配置ipython等调试工具的快捷键,配置类似!

“F1”,进入如下python IDE界面
这里写图片描述
“F5”,进入如下执行程序界面
这里写图片描述

  • AutoPep8 调整Python代码,使其符合PEP8的要求(ctrl+shift+8)
  • SideBarEnhancements侧边栏增强
  • SublimeTmpl插件,新建以预设好的Python模板文件,但是该插件功能没有FileHeader功能强大
  • FileHeader能够自动的监测创建新文件动作,自动的添加模板。因此你可以用别的插件创建新文件,FileHeader会自动的给你添加模板
    这里写图片描述
  • Terminal 调出Windows的控制台,用于各种命令操作
    Preferences->Package Settings->Terminal->settings user中配置:
1
2
3
4
swift复制代码{
"terminal": "C:\\Windows\\System32\\cmd.exe",
"parameters": ["/START","%CWD%"]
}

保存即可,使用快捷键ctrl+shift+t打开cmd命令窗口
这里写图片描述

  • Anaconda代码自动补齐
    直接在package control 搜索安装即可,安装完成后,可适当根据自己习惯进行配置,Anaconda ->settings user配置如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
json复制代码{
//由于Anaconda插件本身无法知道Python安装的路径,所以需要设置Python主程序的实际位置
"python_interpreter": "C:/Users/AppData/Local/Programs/Python/Python36-32/python.exe",
//忽略各种空格不对, 超过79字, import的函数没有使用的提醒,
"pep8_ignore": ["E501", "W292", "E303", "W391", "E225", "E302", "W293", "E402"],
"pyflakes_explicit_ignore":
[
"UnusedImport"
],
//保存文件后自动pep8格式化
"auto_formatting": true,
"auto_formatting_timeout": 5,
//库函数的提示
"enable_signatures_tooltip": true,
"merge_signatures_and_doc":true,

//ST3也有自动补全提示,但只提示文件中输入过的单词,这个功能可用提示变量可用的函数等。
"suppress_word_completions": true,
"suppress_explicit_completions": true,
"complete_parameters": true,
//代码排版时,行的默认长度太短,根据喜好设置
"pep8_max_line_length": 120,


}

配置完成后,即可使用代码自动提示功能!详细的配置可以参考官方文档:
damnwidget.github.io/anaconda/ID…

  • SublimeGit 可以使用git的常用命令

安装前需要在电脑上安装git,然后将git的bin目录配置在环境变量中,此处不详述;然后使用package control 安装SublimeGit即可!安装后,使用ctrl+shift+p输入git命令即可!

这里写图片描述

  • Sublime GitHub可以直接在Sublime中打开与GitHub关联的网址
    这里写图片描述
  • SublimeHighlight
    打开Package Control,输入 Add Repository,在下方的url地址栏输入:
1
bash复制代码https://github.com/n1k0/SublimeHighlight/tree/python3

回车,即可安装SublimeHighlight插件。

其他配置:

Preferences -> Settings - User

1
2
3
4
5
6
7
8
9
10
11
json复制代码{
"ignored_packages":
[
"Vintage"
],
"tab_size": 4,
"translate_tabs_to_spaces": true,
"word_wrap": true,
"update_check": false,
"font_size": 12
}
  • 删除已安装的插件

如果已经安装了某些插件,发现不喜欢,可以删除已经安装的插件,进入package control,搜索remove package,进入后选择要删除的插件即可!

更多插件请参考python.jobbole.com/81312/,查看是否…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…652653654…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%