Plusaber's Blog

  • Home

  • Tags

  • Categories

  • Archives

Singleton pattern

Posted on 2014-09-16 | In Design Pattern | Comments:

Singleton pattern

看到一篇很好的总结关于Java实现线程安全模式的博文,这里直接refer一下,方便后面查看,原文请参考这里:如何正确地写出单例模式.

懒汉式,线程不安全

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {
private static Singleton instance;
private Singleton (){}

public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

这段代码简单明了,而且使用了懒加载模式,但是却存在致命的问题。当有多个线程并行调用 getInstance() 的时候,就会创建多个实例。也就是说在多线程下不能正常工作。

懒汉式,线程安全

为了解决上面的问题,最简单的方法是将整个 getInstance() 方法设为同步(synchronized)。

1
2
3
4
5
6
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}

虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这就引出了双重检验锁。

双重检验锁

双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。程序员称其为双重检查锁,因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。

1
2
3
4
5
6
7
8
9
10
public static Singleton getSingleton() {
if (instance == null) { //Single Checked
synchronized (Singleton.class) {
if (instance == null) { //Double Checked
instance = new Singleton();
}
}
}
return instance ;
}

这段代码看起来很完美,很可惜,它是有问题。主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。

  1. 给instance 分配内存
  2. 调用Singleton的构造函数来初始化成员变量
  3. 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

我们只需要将 instance 变量声明成 volatile 就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton {
private volatile static Singleton instance; //声明成 volatile
private Singleton (){}

public static Singleton getSingleton() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

有些人认为使用 volatile 的原因是可见性,也就是可以保证线程在本地不会存有 instance 的副本,每次都是去主内存中读取。但其实是不对的。使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。

但是特别注意在 Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。

相信你不会喜欢这种复杂又隐含问题的方式,当然我们有更好的实现线程安全的单例模式的办法。

饿汉式 static final field

这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。

1
2
3
4
5
6
7
8
9
10
public class Singleton{
//类加载时就初始化
private static final Singleton instance = new Singleton();

private Singleton(){}

public static Singleton getInstance(){
return instance;
}
}

这种写法如果完美的话,就没必要在啰嗦那么多双检锁的问题了。缺点是它不是一种懒加载模式(lazy initialization),单例会在加载类后一开始就被初始化,即使客户端没有调用 getInstance()方法。饿汉式的创建方式在一些场景中将无法使用:譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。

静态内部类 static nested class

我比较倾向于使用静态内部类的方法,这种方法也是《Effective Java》上所推荐的。

1
2
3
4
5
6
7
8
9
public class Singleton {  
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

这种写法仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。

枚举 Enum

用枚举写单例实在太简单了!这也是它最大的优点。下面这段代码就是声明枚举实例的通常做法。

1
2
3
public enum EasySingleton{
INSTANCE;
}

我们可以通过EasySingleton.INSTANCE来访问实例,这比调用getInstance()方法简单多了。创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。但是还是很少看到有人这样写,可能是因为不太熟悉吧。

总结

一般来说,单例模式有五种写法:懒汉、饿汉、双重检验锁、静态内部类、枚举。上述所说都是线程安全的实现,文章开头给出的第一种方法不算正确的写法。

一般情况下直接使用饿汉式就好了,如果明确要求要懒加载(lazy initialization)会倾向于使用静态内部类,如果涉及到反序列化创建对象时会试着使用枚举的方式来实现单例。

Lucene(2)_Index

Posted on 2014-08-26 | In Developing | Comments:

Lucene的文档建模

文档是Lucene索引和检索的基本单位。文档为一个包含一个或多个field(域)的容器,如title, keywords, author, summary, content等,而field的内容才是真正被索引和检索的内容(经过分解为tokens之后)。如下面的文档(已经建模和区分为各个field):

1
2
3
4
5
6
7
8
title : hello, world
key world: blog, hello
author: zebangchen
content: I have always believed that the man who has
begun to live more seriously within begins to live
more simply without. In an age of extravagance and
waste, I wish I could show to the world how few the
real wants of humanity are.

在对原始文档进行索引时,首先需要将数据转换为Lucene所能识别的文档和field。在随后的搜索过程中,被搜索对象为field的内容,如搜索title : lucene时,搜索结果为title域包含单词lucene的所有文档。

进一步每个域(field)可以进行下面的操作:

  • field的内容(域值)可以不被检索。
  • field被索引后,可以选择性存储项向量(term vector),也就是只是针对这个field的内容的索引。
  • 域值可以被单独存储。

Index过程

Lucene_2

提取plain text和创建document

索引的第一步就是从源文件获取文档然后创建文档,因为很多源文件并不是plain text,例如pdf,xml,html带有大量标签的文档,microsoft文档等。Lucene提供的Tika框架提供了从各个格式文件提取plain text的工具。

分析文档

下一步是建立lucene文档及其field,然后将document通过IndexWriter对象的addDocument传递给Lucene进行索引操作。首先是分析各个field的内容分割为tokens,这一步可以有很多可选操作,如toLowerCase,去stopword等,同样还需要处理tokens,例如stem操作等。最后得到的各个field的tokens会被用于建立index。

向索引添加文档

分析得到的token,文档会被用于建立倒排索引(inverted index)。

Lucene的索引数据结构非常丰富和强大,这里只做一个简要的介绍。Lucene索引包含一个或多个segment(段)。每个段都是一个独立的索引,索引了一部分文档,也就是每个段索引的文档都是不同的,是整个文档集合的一个子集。当索引了一部分文档后,由于内存限制或其他原因,我们需要刷新缓存区的内容将其写入到磁盘中,一个新的段就会被建立,其中包含这部分文档的索引。

在搜索索引时,会访问每个段,然后合并在这些索引段的结果并返回。

一般每个段(索引)都包含多个文件,格式为_x.扩展名,X表示段名称,扩展名用来表示索引的各个不同类型文件(项向量(term vector), 存储的域(stored field), 倒排索引(inverted index))。也可以设置使用混合文件格式,则会将这些不同类型的文件都压缩为一个单一的文件:_X.cfs,这中哦该方式能在搜索期间减少打开文件的数量。

另外还有一个特殊文件,为段文件(segments file),表示为_<N>。该文件指向其他所有正在使用的段。Lucene在检索时,首先会打开该文件,然后依次打开其所指向的文件。N是一个整数,称为the generation,Lucene每次向index提交更新时N都会被加一。

随着时间推移,索引会有越来越多的段,特别是程序打开和关闭writter频繁时。根据设置IndexWriter类会周期性的合并一些段,合并段的选取策略由MergePolicy类决定。

基本索引操作

向索引添加文档

  • addDocument(Document) — 使用默认分析器添加文档,该分析器在创建IndexWriter对象时指定,用于指定将plain text拆分为tokens(tokenization)的策略。
  • addDocument(Document, Analyzer) — 使用指定的分析器进行tokenization。
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
import junit.framework.TestCase;

import lia.common.TestUtil;

import org.apache.lucene.store.Directory;
import org.apache.lucene.store.RAMDirectory;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.analysis.WhitespaceAnalyzer;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.index.Term;

import java.io.IOException;

// From chapter 2
public class IndexingTest extends TestCase {
protected String[] ids = {"1", "2"};
protected String[] unindexed = {"Netherlands", "Italy"};
protected String[] unstored = {"Amsterdam has lots of bridges",
"Venice has lots of canals"};
protected String[] text = {"Amsterdam", "Venice"};

private Directory directory;

protected void setUp() throws Exception { //1
directory = new RAMDirectory();

IndexWriter writer = getWriter(); //2

for (int i = 0; i < ids.length; i++) { //3
Document doc = new Document();
doc.add(new Field("id", ids[i],
Field.Store.YES,
Field.Index.NOT_ANALYZED));
doc.add(new Field("country", unindexed[i],
Field.Store.YES,
Field.Index.NO));
doc.add(new Field("contents", unstored[i],
Field.Store.NO,
Field.Index.ANALYZED));
doc.add(new Field("city", text[i],
Field.Store.YES,
Field.Index.ANALYZED));
writer.addDocument(doc);
}
writer.close();
}

private IndexWriter getWriter() throws IOException { // 2
return new IndexWriter(directory, new WhitespaceAnalyzer(), // 2
IndexWriter.MaxFieldLength.UNLIMITED); // 2
}

protected int getHitCount(String fieldName, String searchString)
throws IOException {
IndexSearcher searcher = new IndexSearcher(directory); //4
Term t = new Term(fieldName, searchString);
Query query = new TermQuery(t); //5
int hitCount = TestUtil.hitCount(searcher, query); //6
searcher.close();
return hitCount;
}

public void testIndexWriter() throws IOException {
IndexWriter writer = getWriter();
assertEquals(ids.length, writer.numDocs()); //7
writer.close();
}

public void testIndexReader() throws IOException {
IndexReader reader = IndexReader.open(directory);
assertEquals(ids.length, reader.maxDoc()); //8
assertEquals(ids.length, reader.numDocs()); //8
reader.close();
}

/*
#1 Run before every test
#2 Create IndexWriter
#3 Add documents
#4 Create new searcher
#5 Build simple single-term query
#6 Get number of hits
#7 Verify writer document count
#8 Verify reader document count
*/

我们需要传入三个变量来创建IndexWiter类:

  • Directory类,索引存储位置。
  • 分析器(analyzer),用于tokenized fields为tokens,进一步用于indexing。
  • MaxFieldLength.UNLIMITED,用于告诉IndexWriter对文档中所有的token建立索引。

IndexWriter类如果检查到之前没有索引在Dicrectory中,则会创建新的索引,否则会将内容加入到存在的索引中。

一旦索引已经被建立或者已经存在,我们就可以循环处理每篇document加入索引。对于每篇plain text文档,我们建立一个Document对象,然后加入其所有的域以及对应的域选项(field options)。

删除索引中的文档

  • deleteDocuments(Term),删除包含term的所有文档(在索引中删除))
  • deleteDocuments(Term[]),删除包含任意一个term的文档
  • deleteDocuments(Query),删除匹配给定query的文档
  • deleteDocuments(Query[]),删除匹配任意一个query的问阿哥
  • deleteAll(),删除index中的所有文档。
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
public void testDeleteBeforeOptimize() throws IOException {
IndexWriter writer = getWriter();
assertEquals(2, writer.numDocs()); //A
writer.deleteDocuments(new Term("id", "1")); //B
writer.commit();
assertTrue(writer.hasDeletions()); //1
assertEquals(2, writer.maxDoc()); //2
assertEquals(1, writer.numDocs()); //2
writer.close();
}

public void testDeleteAfterOptimize() throws IOException {
IndexWriter writer = getWriter();
assertEquals(2, writer.numDocs());
writer.deleteDocuments(new Term("id", "1"));
writer.optimize(); //3
writer.commit();
assertFalse(writer.hasDeletions());
assertEquals(1, writer.maxDoc()); //C
assertEquals(1, writer.numDocs()); //C
writer.close();
}

/*
#A 2 docs in the index
#B Delete first document
#C 1 indexed document, 0 deleted documents
#1 Index contains deletions
#2 1 indexed document, 1 deleted document
#3 Optimize compacts deletes
*/

在执行delete后,真正的删除操作并不会马山执行,而是放入内存缓冲区。同样我们要调用writter的commit()或close()来执行实际的删除操作。

更新索引中的文档

Lucene无法只更新文档的某个域,而是删除旧文档,然后向索引中添加新问昂。

  • updateDocument(Term, Document),首先删除包含term的所有文档,然后使用writter的默认分析器添加新文档
  • updateDocument(Term, Document, Analyzer),与上面功能一致,区别是指定分析器。
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

public void testUpdate() throws IOException {

assertEquals(1, getHitCount("city", "Amsterdam"));

IndexWriter writer = getWriter();

Document doc = new Document(); //A
doc.add(new Field("id", "1",
Field.Store.YES,
Field.Index.NOT_ANALYZED)); //A
doc.add(new Field("country", "Netherlands",
Field.Store.YES,
Field.Index.NO)); //A
doc.add(new Field("contents",
"Den Haag has a lot of museums",
Field.Store.NO,
Field.Index.ANALYZED)); //A
doc.add(new Field("city", "Den Haag",
Field.Store.YES,
Field.Index.ANALYZED)); //A

writer.updateDocument(new Term("id", "1"), //B
doc); //B
writer.close();

assertEquals(0, getHitCount("city", "Amsterdam"));//C
assertEquals(1, getHitCount("city", "Haag")); //D
}

/*
#A Create new document with "Haag" in city field
#B Replace original document with new version
#C Verify old document is gone
#D Verify new document is indexed
*/

域选项(field options)

索引数字、日期和时间

优化索引

其他Directory子类

并发、线程安全及锁机制

高级索引概念

Lucene(1)_Introduction

Posted on 2014-08-09 | In Developing | Comments:

Introduction

Lucene是一个强大的开源信息检索工具库,通过Lucene我们可以轻易将搜索功能加到我们的应用程序中。

通常一个搜索程序需要包含的组件如下:

其中Lucene为深色部分的组件提供了强大的可扩展的工具库。

在获取内容后,为了对这些内容进行高效检索,所需要的做的就是为这些文档建立索引,而在建立索引之前,我们需要分析文档,以把文档分解为token的集合,然后对这些token建立索引。而分解文档为token就是Lucene的第一个任务,有许多问题需要在这一步解决,比如如何处理词组的问题,如何处理拼写错误,如何处理同义词关系等。对于中文等语言,甚至词与词之间都没有边界,这时还需要进行中文分词。

Lucene提供了许多分析器可以让我们轻松定制所需要的文档分析器。

在文档分析后, 我们就可以对文档建立索引,用于高效检索。Lucene也提供了强大的支持。

对于搜索功能,通常是客户提交一个搜索请求,然后系统根据请求返回文档。Lunece提供了一个称为查询解析器的(QueryParser)的开发包用于处理用户的请求。查询请求可以包含布尔运算、短语查询或通配符查询。下一步是根据解析后的查询,结合前面建立的索引得到匹配查询的文档。这一系列非常复杂,Lucene同样提供了强大的支持,可以让我们轻松实现结果检索、过滤、排序等功能。

常见的搜索模型有如下3种:

  • 纯布尔模型(pure boolean model) — 只检查查询与文档是否匹配,没有评分,没有排序。
  • 向量空间模型(vector space model) — query和document都作为基于token空间的向量模型,通过计算向量距离作为匹配概率,并用于排序。
  • 概率模型(probabilistic model) — 采用全概率方法来计算文档和查询语句的匹配概率。

Example

下面是一个对指定文件夹以.txt结尾的文件进行index的代码:

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
package lia.meetlucene;

import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.util.Version;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.FileReader;

// From chapter 1

/**
* This code was originally written for
* Erik's Lucene intro java.net article
*/
public class Indexer {

public static void main(String[] args) throws Exception {
if (args.length != 2) {
throw new IllegalArgumentException("Usage: java " + Indexer.class.getName()
+ " <index dir> <data dir>");
}
String indexDir = args[0]; //1
String dataDir = args[1]; //2

long start = System.currentTimeMillis();
Indexer indexer = new Indexer(indexDir);
int numIndexed;
try {
numIndexed = indexer.index(dataDir, new TextFilesFilter());
} finally {
indexer.close();
}
long end = System.currentTimeMillis();

System.out.println("Indexing " + numIndexed + " files took "
+ (end - start) + " milliseconds");
}

private IndexWriter writer;

public Indexer(String indexDir) throws IOException {
Directory dir = FSDirectory.open(new File(indexDir));
writer = new IndexWriter(dir, //3
new StandardAnalyzer( //3
Version.LUCENE_30),//3
true, //3
IndexWriter.MaxFieldLength.UNLIMITED); //3
}

public void close() throws IOException {
writer.close(); //4
}

public int index(String dataDir, FileFilter filter)
throws Exception {

File[] files = new File(dataDir).listFiles();

for (File f: files) {
if (!f.isDirectory() &&
!f.isHidden() &&
f.exists() &&
f.canRead() &&
(filter == null || filter.accept(f))) {
indexFile(f);
}
}

return writer.numDocs(); //5
}

private static class TextFilesFilter implements FileFilter {
public boolean accept(File path) {
return path.getName().toLowerCase() //6
.endsWith(".txt"); //6
}
}

protected Document getDocument(File f) throws Exception {
Document doc = new Document();
doc.add(new Field("contents", new FileReader(f))); //7
doc.add(new Field("filename", f.getName(), //8
Field.Store.YES, Field.Index.NOT_ANALYZED));//8
doc.add(new Field("fullpath", f.getCanonicalPath(), //9
Field.Store.YES, Field.Index.NOT_ANALYZED));//9
return doc;
}

private void indexFile(File f) throws Exception {
System.out.println("Indexing " + f.getCanonicalPath());
Document doc = getDocument(f);
writer.addDocument(doc); //10
}
}

/*
#1 Create index in this directory
#2 Index *.txt files from this directory
#3 Create Lucene IndexWriter
#4 Close IndexWriter
#5 Return number of documents indexed
#6 Index .txt files only, using FileFilter
#7 Index file content
#8 Index file name
#9 Index file full path
#10 Add document to Lucene index
*/

简单的搜索程序:

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
package lia.meetlucene;

import org.apache.lucene.document.Document;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.util.Version;

import java.io.File;
import java.io.IOException;

// From chapter 1

/**
* This code was originally written for
* Erik's Lucene intro java.net article
*/
public class Searcher {

public static void main(String[] args) throws IllegalArgumentException,
IOException, ParseException {
if (args.length != 2) {
throw new IllegalArgumentException("Usage: java " + Searcher.class.getName()
+ " <index dir> <query>");
}

String indexDir = args[0]; //1
String q = args[1]; //2

search(indexDir, q);
}

public static void search(String indexDir, String q)
throws IOException, ParseException {

Directory dir = FSDirectory.open(new File(indexDir)); //3
IndexSearcher is = new IndexSearcher(dir); //3

QueryParser parser = new QueryParser(Version.LUCENE_30, // 4
"contents", //4
new StandardAnalyzer( //4
Version.LUCENE_30)); //4
Query query = parser.parse(q); //4
long start = System.currentTimeMillis();
TopDocs hits = is.search(query, 10); //5
long end = System.currentTimeMillis();

System.err.println("Found " + hits.totalHits + //6
" document(s) (in " + (end - start) + // 6
" milliseconds) that matched query '" + // 6
q + "':"); // 6

for(ScoreDoc scoreDoc : hits.scoreDocs) {
Document doc = is.doc(scoreDoc.doc); //7
System.out.println(doc.get("fullpath")); //8
}

is.close(); //9
}
}

/*
#1 Parse provided index directory
#2 Parse provided query string
#3 Open index
#4 Parse query
#5 Search index
#6 Write search stats
#7 Retrieve matching document
#8 Display filename
#9 Close IndexSearcher
*/

索引过程的核心类

  • IndexWriter
    这个类负责创建新索引或者打开已有索引,以及向索引中添加、删除或更新被索引文档的信息。可以将IndexWritter看做为索引写入操作提供支持的类。IndexWritter需要开辟空间来存储索引,该功能有Directory完成。

  • Directory
    Directory类描述了索引的存放位置,这是一个抽象类,它子类负责具体指向索引的存储路径,如前面例子中的FSDirectory.open方法来获取真实路径。

  • Analyzer
    文本文件在被索引之前需要经过Analyzer处理。Analyzer在IndexWriter构造器中被指定,负责将文档拆分为tokens,用于index。Analyzer同样是一个抽象类,有很多子类负责不同具体的实现。

  • Document
    Document代表一篇文档,但不是原始的text文档,而是抽象文档—fields的集合,fields表示文档的的一些元数据,例如标题,作者,创立日期,summary,filename,first paragraph等。不同的的元数据都作为文档不同的field单独存储并被索引。在不同field的相同token具有不同意义和重要性,比如title的更加重要。在搜索时我们也可以指定token一定要出现在某个field。— Document即是一个包含多个Field对象的容器,Field是一个包含能被索引的文本内容的类。

  • Field
    索引中的每个文档都包含一个或多个不同的field(域),每个field都有一个名字(域名)和对应的内容(text),以及一组选项说明lucene如何index这个field的内容。文档可以拥有多个同名的field,但是在建立索引时,这些field内容按照顺序被处理,就像被连接在一起作为一个text处理。

搜索过程的核心类

IndexSearcher
IndexSearcher用于搜索索引。最基本的使用是传入一个query对象和top N参数,返回一个TopDocs对象包含若干结果。

1
2
3
4
5
Directory dir = FSDirectory.open(new File("/tmp/index"));
IndexSearcher searcher = new IndexSearcher(dir);
Query q = new TermQuery(new Term("contents", "lucene"));
TopDocs hits = searcher.search(q, 10);
searcher.close();
  • Term
    Term对象是搜索功能的基本单元,与Field对象十分类似,只不过一个是query的组成单元,一个是Document的组成单元。Term同样包含一对字符串元素:名字和内容(text)。
1
2
Query q = new TermQuery(new Term("contents", "lucene"));
TopDocs hits = searcher.search(q, 10);

上面代码表示寻找contexts域(field)包含单词lucene的前10个document。

  • Query
    Query类对象是查询的参数,也是一个抽象类,具体实现有TermQuery, BooleanQuery, PhraseQuery…

  • TermQuery
    TermQuery是最基本最简单的查询类型,用来匹配指定域(field)中包含特定内容的文档。

  • TopDocs
    TopDocs类是一个简单的指针容器,指向前N个排名的搜索结果。TopDocs记录前N个结果的int docID和浮点型分数。

Java泛型

Posted on 2014-08-05 | In java | Comments:

Java泛型

感觉java泛型设计得挺复杂,特别是泛型,泛型通配符,泛型上限下限等搅在一起时,实在不太好区分,容易出错。不管怎样,泛型对于设计可复用和扩展的代码支持还是很强大的,这里总结一下自己的理解。《疯狂java讲义》讲解的非常好,想深入理解可以参考该书,另外后面的代码也是参考该书。

泛型就是指在定义类,借口,方法使用类型形参,这个类型形参将在声明变量,创建对象,调用方法动态地指定,可以极强的增强代码的通用性。

理解泛型

定义泛型接口,类

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface List<E>
{
void add(E x);
Iterator<E> iterator();
}

public interface Iterator<E>
{
E next();
boolean hasNext();
}

public interface Map<K, V>
{
Set<K> keySet();
V put(K key, V value);
}
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
public class Apple<T>
{
// 使用T类型形参定义实例变量
private T info;
public Apple(){}
// 下面方法中使用T类型形参来定义构造器
public Apple(T info)
{
this.info = info;
}
public void setInfo(T info)
{
this.info = info;
}
public T getInfo()
{
return this.info;
}
public static void main(String[] args)
{
// 因为传给T形参的是String实际类型,
// 所以构造器的参数只能是String
Apple<String> a1 = new Apple<>("苹果");
System.out.println(a1.getInfo());
// 因为传给T形参的是Double实际类型,
// 所以构造器的参数只能是Double或者double
Apple<Double> a2 = new Apple<>(5.67);
System.out.println(a2.getInfo());
}
}

从泛型类派生子类

为Apple派生子类时,不能在包含形参,下面代码是错误的:

1
public class A extends Apple<T>{}

必须要改为:

1
public class A extends Apple<String>{}

或者

1
public class A extends Apple{}

需要注意的是,这时系统会把Apple<T>类里的T当做Object类型来处理。

并不存在泛型类

ArrayList<String>只是类似于一种逻辑类,并不是真正产生了一个新的class文件或新的类。

不管为泛型的类型形参传入哪一种类型实参,对于Java来说它们依然被当做同一个类处理,在内存也只占用一块内存空间,因此在静态方法,静态初始化快或者静态变量的声明和初始化中不允许使用类型形参。

1
2
3
4
5
6
7
8
9
public class R<T>
{
// 下面代码错误,不能在静态Field声明中使用类型形参
// static T info;
T age;
public void foo(T msg){}
// 下面代码错误,不能在静态方法声明中使用类型形参
// public static void bar(T msg){}
}

另外由于系统中并不会真正生成泛型类,所以instanceof运算符后不能使用泛型类:

1
2
Collection<String> cs = new ArrayList<>();
if(cs instanceof ArrayList<String>){...} //引发编译错误

类型通配符

List<Integer> 并不是List<Object>的子类型!
也就是:

1
2
3
4
5
public void test(List<Object> c) {
for(int i=0; i<c.size(), i++) {
System.out.println(c.get(i));
}
}

并不能输入List<Integer> c作为参数。

使用类型通配 —(?)

为了表示各种泛型List的父类(并不是真正的父类,只是一种类似的逻辑关系),可以使用类型通配符,类型通配符是一个问号(?),将一个问号作为类型实参传给List集合,写作List<?>,意思是元素类型未知的List。这个?被称为通配符,它的元素类型可以匹配任何类型。

也就是,在 public void test(List<?> c)的函数,可以输入List<Integer> c,List<String> c,List<Character> c作为参数。

可以通过这种带通配符的List遍历元素,但是,不能这种带通配符的List将元素加入到其中:

1
2
3
4
5
6
List<Integer> list = new ArrayList<Integer>();
List<?> c = list;
//或者 List<?> c = new ArrayList<String>();

c.add(new Object()); // 应发编译错误
c.add(12); // 引发编译错误

原因是系统无法确定c集合中元素的类型,所以不能向其中添加对象。根绝前面List<E>接口的定义代码可以发现,add()方法需要有类型参数E作为集合元素的类型,传给add的参数必须是E类的对象或者其子类的对象,但在这中情况下,因为并不知道E是什么类型(?),所以程序无法判断尝试放入的对象是否合法,所以无法放入元素。唯一的例外是null。

另一方面程序可以调用get()方法来返回List<?>集合指定索引处的元素,其返回值一个未知类的元素,但可以肯定的是,它总是一个Object对象,所以一定可以用Object接受,如果知道其类型,也可以使用类型转换转到所需要的对象类型。

设定类型通配符的上限

当直接使用List<?>这种形式时,即表明这个List集合可以是任何泛型List的父类。但在某些情况,我们不希望这个List是任何List的父类,而是代表某一类泛型List的父类。

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
// 定义一个抽象类Shape
public abstract class Shape
{
public abstract void draw(Canvas c);
}

// 定义Shape的子类Rectangle
public class Rectangle extends Shape
{
// 实现画图方法,以打印字符串来模拟画图方法实现
public void draw(Canvas c)
{
System.out.println("把一个矩形画在画布" + c + "上");
}
}

// 定义Shape的子类Circle
public class Circle extends Shape
{
// 实现画图方法,以打印字符串来模拟画图方法实现
public void draw(Canvas c)
{
System.out.println("在画布" + c + "上画一个圆");
}
}

public class Canvas
{
// 同时在画布上绘制多个形状
// public void drawAll(List<Shape> shapes)
// {
// for (Shape s : shapes)
// {
// s.draw(this);
// }
// }
// public void drawAll(List<?> shapes)
// {
// for (Object obj : shapes)
// {
// Shape s = (Shape)obj;
// s.draw(this);
// }
// }
// 同时在画布上绘制多个形状,使用被限制的泛型通配符
public void drawAll(List<? extends Shape> shapes)
{
for (Shape s : shapes)
{
s.draw(this);
}
}



public static void main(String[] args)
{
List<Circle> circleList = new ArrayList<Circle>();
Canvas c = new Canvas();
// 由于List<Circle>并不是List<Shape>的子类型,
// 所以下面代码引发编译错误
c.drawAll(circleList);
}
}

List<? extends Shape>是受限制通配符的例子,此处?同样代表一个未知的类型,但是此处的未知类型一定是Shape的子类型(也可以是Shape本身),因此可以把Shape称为这个通配符的上限(upper bound).

这时drawAll(List<? extends Shape> shapes)的参数就限定了泛型参数必须为Shape或者其子类,而不能是其他,例如List c就不能作为drawAll的参数,否则会引发编译错误。

类似的,由于系统无法确定这个受限制的通配符的具体类型,所以不能不能通过shapes把对象添加到这个泛型中,即使是Shape的对象或子类对象:

1
2
3
4
public void addRectangle(List<? extends Shape> shapes)
{
shapes.add(new Rectangle()); //编译错误
}

设定类型形参的上限

Java泛型不仅允许在使用通配符形参时设定上限,而且可以在定义类型形参时设定上限,用于表示传给该类型形参的实际类型要么是该上限类型,要么是该上限类型的子类。

1
2
3
4
5
public class Apple<T extends Number>
{
T col;
....
}

泛型方法

在定义类,接口时没有使用类型形参,但定义方法时想自己定义类型形参,这时可以使用泛型方法。

定义泛型方法

考虑实现这样一个方法,该方法将一个Object数组的所有元素添加到一个Collection集合中。

1
2
3
4
5
fromArrayToCollection(Object[] a, Collection<Object> c) {
for(Object o : a ) {
c.add(o);
}
}

问题,并不能使用Collection, 同样不能使用:

1
2
3
4
5
fromArrayToCollection(Object[] a, Collection<?> c) {
for(Object o : a ) {
c.add(o); // 编译错误
}
}

使用泛型方法解决问题:

1
修饰符 <T, S> 返回值类型 方法明<形参列表> {}
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
public class GenericMethodTest
{
// 声明一个泛型方法,该泛型方法中带一个T类型形参,
static <T> void fromArrayToCollection(T[] a, Collection<T> c)
{
for (T o : a)
{
c.add(o);
}
}
public static void main(String[] args)
{
Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<>();
// 下面代码中T代表Object类型
<Object> fromArrayToCollection(oa, co);
String[] sa = new String[100];
Collection<String> cs = new ArrayList<>();
// 下面代码中T代表String类型
fromArrayToCollection(sa, cs);
// 下面代码中T代表Object类型
fromArrayToCollection(sa, co);
Integer[] ia = new Integer[100];
Float[] fa = new Float[100];
Number[] na = new Number[100];
Collection<Number> cn = new ArrayList<>();
// 下面代码中T代表Number类型
fromArrayToCollection(ia, cn);
// 下面代码中T代表Number类型
fromArrayToCollection(fa, cn);
// 下面代码中T代表Number类型
fromArrayToCollection(na, cn);
// 下面代码中T代表Object类型
fromArrayToCollection(na, co);
// 下面代码中T代表String类型,但na是一个Number数组,
// 因为Number既不是String类型,
// 也不是它的子类,所以出现编译错误
//fromArrayToCollection(na, cs);
}
}

与类,接口使用泛型参数不同的是,方法中的泛型参数,方法中的泛型参数无需显式传入实际类型参数,如上面所示,调用方法时,无需在调用方法前传入String,Object等类型,系统完成推断,但是如果传入的参数的类型不同,系统则无法准确判断该使用哪个类型,出现错误。

如果传入的参数有不同的形参,但是属于同一类型,可以借助带上限的通配符解决。否则需要声明多个泛型参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class RightTest
{
// 声明一个泛型方法,该泛型方法中带一个T形参
static <T> void test(Collection<? extends T> from , Collection<T> to)
{
for (T ele : from)
{
to.add(ele);
}
}
public static void main(String[] args)
{
List<Object> ao = new ArrayList<>();
List<String> as = new ArrayList<>();
// 下面代码完全正常
test(as , ao);
}
}

泛型方法和类型通配符的区别

大多数时候都可以使用泛型方法来代替类型通配符,例如:

1
2
3
4
5
6
7
8
9
10
public interface Collection<E> {
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
}

public interface Collection<E>
{
<T> boolean containAll(Collection<T> c);
<T extends E> boolean addAll(Collection<T> c);
}

上面两个方法类型形参T只使用了依次,产生的唯一效果是可以在不同的调用点传入不同的实际类型,对于这种情况,应该使用通配符:通配符就是被设计用来支持灵活的子类化的。

泛型方法允许类型形参被用来表示方法的一个或者多个参数之间的类型依赖关系,或者方法返回值与参数之间的类型依赖关系,另外还支持添加元素。如果没有这样的类型依赖关系,就不应该使用泛型方法。

如果有需要,也可以同时使用泛型方法和通配符,如Java的Collections.copy():

1
2
3
4
public class Collections
{
public static <T> void copy(List<T> dest, List<? extends T> src){...}
}

Java 7的”菱形”语法与泛型构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Foo
{
public <T> Foo(T t)
{
System.out.println(t);
}
}
public class GenericConstructor
{
public static void main(String[] args)
{
// 泛型构造器中的T参数为String。
new Foo("疯狂Java讲义");
// 泛型构造器中的T参数为Integer。
new Foo(200);
// 显式指定泛型构造器中的T参数为String,
// 传给Foo构造器的实参也是String对象,完全正确。
new <String> Foo("疯狂Android讲义");
// 显式指定泛型构造器中的T参数为String,
// 传给Foo构造器的实参也是Double对象,下面代码出错
new <String> Foo(12.3);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyClass<E>
{
public <T> MyClass(T t)
{
System.out.println("t参数的值为:" + t);
}
}
public class GenericDiamondTest
{
public static void main(String[] args)
{
// MyClass类声明中的E形参是String类型。
// 泛型构造器中声明的T形参是Integer类型
MyClass<String> mc1 = new MyClass<>(5);
// 显式指定泛型构造器中声明的T形参是Integer类型,
MyClass<String> mc2 = new <Integer> MyClass<String>(5);
// MyClass类声明中的E形参是String类型。
// 如果显式指定泛型构造器中声明的T形参是Integer类型
// 此时就不能使用"菱形"语法,下面代码是错的。
//MyClass<String> mc3 = new <Integer> MyClass<>(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
public class MyUtils
{
// 下面dest集合元素类型必须与src集合元素类型相同,或是其父类
public static <T> T copy(Collection<? super T> dest
, Collection<T> src)
{
T last = null;
for (T ele : src)
{
last = ele;
dest.add(ele);
}
return last;
}
public static void main(String[] args)
{
List<Number> ln = new ArrayList<>();
List<Integer> li = new ArrayList<>();
li.add(5);
// 此处可准确的知道最后一个被复制的元素是Integer类型
// 与src集合元素的类型相同
Integer last = copy(ln , li); // ①
System.out.println(ln);
}
}

搽除和转换

在严格的泛型代码里,带泛型声明的类总应该带着类型参数。但为了与老的Java代码保持一致,也允许在使用泛型声明的类时不指定实际的类型参数。如果没有为这个泛型类指定实际的类型参数,则该类型参数被称作raw type,默认是声明该类型参数时制定的第一个个上限类型。

当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量是,所有在尖括号之间的类型信息都将被扔掉。比如一个List<String>类型被转换为List,则该List对集合元素的类型检查变成了类型参数的上限(即Object)。List<Integer>则变成Number,下面程序示范了这种搽除。

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
class Apple<T extends Number>
{
T size;
public Apple()
{
}
public Apple(T size)
{
this.size = size;
}
public void setSize(T size)
{
this.size = size;
}
public T getSize()
{
return this.size;
}
}
public class ErasureTest
{
public static void main(String[] args)
{
Apple<Integer> a = new Apple<>(6); //①
// a的getSize方法返回Integer对象
Integer as = a.getSize();
// 把a对象赋给Apple变量,丢失尖括号里的类型信息
Apple b = a; //②
// b只知道size的类型是Number
Number size1 = b.getSize();
// 下面代码引起编译错误
Integer size2 = b.getSize(); //③
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ErasureTest2
{
public static void main(String[] args)
{
List<Integer> li = new ArrayList<>();
li.add(6);
li.add(9);
List list = li;
// 下面代码引起“未经检查的转换”的警告,编译、运行时完全正常
List<String> ls = list; // ①
// 但只要访问ls里的元素,如下面代码将引起运行时异常。
System.out.println(ls.get(0));
}
}

泛型与数组

Java不支持创建泛型数组,数组元素的类型不能包含类型变量或类型形参,除非是无上限的类型通配符。但可以声明元素包含类型变量或者类型形参的数组(相当晕。。)。也就是说,可以声明List<String>[]形式的数组,但不能创建ArrayList<String>[10]这样的数组对象。另外特殊的是可以建立无上限的通配符泛型数组,例如new ArrayList<?>[10]。在这种情况下,程序不得不进行强制类型转换。

创建元素类型是类型变量的数组对象也将导致编译错误:

1
2
3
4
<T> T[] makeArray(Collection<T> coll)
{
return new T[coll.size()];
}

Java多线程

Posted on 2014-08-02 | In java | Comments:

Java多线程编程

Java线程的创建和启动

继承Thread类创建线程类

  1. 继承Thread类,并重写run()方法,该run()方法的方法体代表线程需要完成的任务,因此把run()方法称为线程执行体。
  2. 创建Thread子类的实例,即线程对象。
  3. 调用线程对象的start()来启动线程。
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
public class FirstThread extends Thread
{
private int i ;
// 重写run方法,run方法的方法体就是线程执行体
public void run()
{
for ( ; i < 100 ; i++ )
{
// 当线程类继承Thread类时,直接使用this即可获取当前线程
// Thread对象的getName()返回当前该线程的名字
// 因此可以直接调用getName()方法返回当前线程的名
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args)
{
for (int i = 0; i < 100; i++)
{
// 调用Thread的currentThread方法获取当前线程
System.out.println(Thread.currentThread().getName()
+ " " + i);
if (i == 20)
{
// 创建、并启动第一条线程
new FirstThread().start();
// 创建、并启动第二条线程
new FirstThread().start();
}
}
}
}

Java程序运行时默认的主线程总共是main()方法的方法体。

  • Thread.currentThread(), Thread类的静态方法,总是返回当前正在执行的线程对象。
  • getName(): Thread类的实例方法,返回调用该方法的线程名字。

使用继承Thread类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量。

实现Runnable接口创建线程类

  1. 定义Runnable接口的实现类,重写run()方法。
  2. 创建Runnable实现类的实例,并依次实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
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
public class SecondThread implements Runnable
{
private int i ;
// run方法同样是线程执行体
public void run()
{
for ( ; i < 100 ; i++ )
{
// 当线程类实现Runnable接口时,
// 如果想获取当前线程,只能用Thread.currentThread()方法。
System.out.println(Thread.currentThread().getName()
+ " " + i);
}
}

public static void main(String[] args)
{
for (int i = 0; i < 100; i++)
{
System.out.println(Thread.currentThread().getName()
+ " " + i);
if (i == 20)
{
SecondThread st = new SecondThread(); // ①
// 通过new Thread(target , name)方法创建新线程
new Thread(st , "新线程1").start();
new Thread(st , "新线程2").start();
}
}
}
}

程序所创建的Runnable对象只是线程的target,多个线程可共享一个target,所以可以共享这个实例的实例变量。

使用Callable和Future创建线程

类似与Runnable接口的用法,Callable接口提供了一个call()方法作为线程执行体,不同的是,call()可以有返回值。,且可以声明抛出异常。

Future接口代表Callable接口call()方法的返回值,并为接口提供了一个FutureTask的实现了,该实现类实现了Future接口,实现了Runnable接口—可以作为Thread类的target。

几个方法:

  • boolean cancle(boolean mayInterruptIfRunning)
  • V get(): 返回Callable任务里call()方法的返回值。调用该方法将导致程序阻塞,必须等到子线程结束后才会得到返回值。
  • V get(long timeout, TimeUnit unit)
  • boolean isCancelled()
  • boolean isDone()

步骤:

  1. 创建Callable接口实现类,并实现call()方法
  2. 使用FutureTask类包装Callable对象
  3. 使用FutureTask对象作为Thread对象的target创建并启动线程
  4. 通过调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
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
public class ThirdThread implements Callable<Integer>
{
// 实现call方法,作为线程执行体
public Integer call()
{
int i = 0;
for ( ; i < 100 ; i++ )
{
System.out.println(Thread.currentThread().getName()
+ " 的循环变量i的值:" + i);
}
// call()方法可以有返回值
return i;
}

public static void main(String[] args)
{
// 创建Callable对象
ThirdThread rt = new ThirdThread();
// 使用FutureTask来包装Callable对象
FutureTask<Integer> task = new FutureTask<Integer>(rt);
for (int i = 0 ; i < 100 ; i++)
{
System.out.println(Thread.currentThread().getName()
+ " 的循环变量i的值:" + i);
if (i == 20)
{
// 实质还是以Callable对象来创建、并启动线程
new Thread(task , "有返回值的线程").start();
}
}
try
{
// 获取线程返回值
System.out.println("子线程的返回值:" + task.get());
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}

线程的生命周期

新建-就绪-运行—(就绪)-运行-阻塞-(就绪)-死亡

新建和就绪状态

当程序使用new关键字创建了一个线程后,该线程就处于新建状态。
只有当线程对象调用了start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,但并没有直接运行,只是可以运行了。至于何时运行,取决于线程调度器的调度。

启动线程要使用start()方法,不是run()方法,否则只是一个普通的方法调用!

运行和阻塞状态

现代操作系统都采用抢占式调度策略,也就是系统会给每个可执行的线程一个小时间片段来处理任务,该时间段用完后,系统就会剥夺该线程所占用的资源,将其放入就绪状态,然其他线程获得执行机会。如果线程执行时被阻塞,系统也会将资源重新给其他线程。

发生如下情况时,线程将会进入阻塞状态:

  • 线程调用sleep()方法主动放弃占用的处理器资源
  • 线程调用了阻塞式IO方法,在该方法返回前,该线程被阻塞。
  • 线程试图获得一个同步监视器,但该监视器正在被其他线程所持有。
  • 线程在等待某个notify
  • 程序调用了线程的suspend()将其挂起。

当前正在执行的线程被阻塞后,其他线程就可以获得执行的机会。被阻塞的线程会在阻塞基础后重新进入就绪状态,等待再次被调用。

针对上面几种情况,下面情况可以解除上面的阻塞:

  • sleep()方法的时间超过
  • 线程调用的阻塞式IO方法已经返回。
  • 线程成功获得试图取得的同步监视器
  • 获得notify
  • 挂起的线程被调用了resume()恢复方法。

注意yield()方法是直接放弃资源重新进入就绪队列。

线程死亡

几种情况:

  • run()或call()方法执行完成。
  • 线程抛出一个未捕获的Exception或Error。
  • 直接调用该线程的stop()方法。

当主线程结束时,其他线程不收任何影响,并不会随之结束。一旦子线程启动起来,就拥有和主线程相同的地位,不会受主线程的影响。

isAlive(),新建,死亡时返回true,其他返回false。

不能再start已经启动并死亡的线程。

控制线程

join线程

Thread提供了让一个线程等待另一个线程执行完的方法—join(). 当某个程序执行流中调用其他线程的join()方法时,调用线程即被阻塞,直到被join()方法加入的join线程执行完为止.

通常有使用线程的程序调用,将大问题划分为许多小问题,每个小问题分配一个线程。

  • join()
  • join(long millis)
  • join(long millis, int nanos)
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
public class JoinThread extends Thread
{
// 提供一个有参数的构造器,用于设置该线程的名字
public JoinThread(String name)
{
super(name);
}
// 重写run方法,定义线程执行体
public void run()
{
for (int i = 0; i < 100 ; i++ )
{
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args)throws Exception
{
// 启动子线程
new JoinThread("新线程").start();
for (int i = 0; i < 100 ; i++ )
{
if (i == 20)
{
JoinThread jt = new JoinThread("被Join的线程");
jt.start();
// main线程调用了jt线程的join()方法,main线程
// 必须等jt执行结束才会向下执行
jt.join();
}
System.out.println(Thread.currentThread().getName()
+ " " + i);
}
}
}

后台进程

后台线程的任务是为其他线程提供服务。如果所有前台线程都死亡,后台线程就会自动死亡。

必须要在启动前设置。

  • setDaemon(true)
  • isDaemon()
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
public class DaemonThread extends Thread
{
// 定义后台线程的线程执行体与普通线程没有任何区别
public void run()
{
for (int i = 0; i < 1000 ; i++ )
{
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args)
{
DaemonThread t = new DaemonThread();
// 将此线程设置成后台线程
t.setDaemon(true);
// 启动后台线程
t.start();
for (int i = 0 ; i < 10 ; i++ )
{
System.out.println(Thread.currentThread().getName()
+ " " + i);
}
// -----程序执行到此处,前台线程(main线程)结束------
// 后台线程也应该随之结束
}
}

线程睡眠:sleep

让当前正在执行的线程暂停一段时间,并进入阻塞状态。

  • static void sleep(long millis)
  • static void sleep(long millis, int nanos)
1
2
3
4
5
6
7
8
9
10
11
12
13
public class SleepTest
{
public static void main(String[] args)
throws Exception
{
for (int i = 0; i < 10 ; i++ )
{
System.out.println("当前时间: " + new Date());
// 调用sleep方法让当前线程暂停1s。
Thread.sleep(1000);
}
}
}

线程让步:yield

放弃资源,但不会进入阻塞状态,有可能马上又得到调用。

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
public class YieldTest extends Thread
{
public YieldTest(String name)
{
super(name);
}
// 定义run方法作为线程执行体
public void run()
{
for (int i = 0; i < 50 ; i++ )
{
System.out.println(getName() + " " + i);
// 当i等于20时,使用yield方法让当前线程让步
if (i == 20)
{
Thread.yield();
}
}
}
public static void main(String[] args)throws Exception
{
// 启动两条并发线程
YieldTest yt1 = new YieldTest("高级");
// 将ty1线程设置成最高优先级
// yt1.setPriority(Thread.MAX_PRIORITY);
yt1.start();
YieldTest yt2 = new YieldTest("低级");
// 将yt2线程设置成最低优先级
// yt2.setPriority(Thread.MIN_PRIORITY);
yt2.start();
}
}

改变线程优先级

setPriority(int newPriority)
getPriotiry()

优先级为1-10之间,最好使用下面几个静态常量

  • MAX_PRIORITY: 10
  • MIN_PRIORITY: 0
  • NORM_PRIORITY: 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
37
38
39
40
41
42
public class PriorityTest extends Thread
{
// 定义一个有参数的构造器,用于创建线程时指定name
public PriorityTest(String name)
{
super(name);
}
public void run()
{
for (int i = 0 ; i < 50 ; i++ )
{
System.out.println(getName() + ",其优先级是:"
+ getPriority() + ",循环变量的值为:" + i);
}
}
public static void main(String[] args)
{
// 改变主线程的优先级
Thread.currentThread().setPriority(6);
for (int i = 0 ; i < 30 ; i++ )
{
if (i == 10)
{
PriorityTest low = new PriorityTest("低级");
low.start();
System.out.println("创建之初的优先级:"
+ low.getPriority());
// 设置该线程为最低优先级
low.setPriority(Thread.MIN_PRIORITY);
}
if (i == 20)
{
PriorityTest high = new PriorityTest("高级");
high.start();
System.out.println("创建之初的优先级:"
+ high.getPriority());
// 设置该线程为最高优先级
high.setPriority(Thread.MAX_PRIORITY);
}
}
}
}

线程同步

由于线程调度的随机性,当多个线程需要访问并修改同一个数据时,很容易出现线程安全问题。

同步代码块

1
2
3
synchronized(obj) {
// 同步代码块
}

obj就是同步监视器,线程在执行同步代码块之前,必须先获得对同步监视器的锁定。

任何时刻只能有一个线程可以获得对同步监视器的锁定,执行完成后,则会释放该同步监视器的锁定。

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
public class DrawThread extends Thread
{
// 模拟用户账户
private Account account;
// 当前取钱线程所希望取的钱数
private double drawAmount;
public DrawThread(String name , Account account
, double drawAmount)
{
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
// 当多条线程修改同一个共享数据时,将涉及数据安全问题。
public void run()
{
// 使用account作为同步监视器,任何线程进入下面同步代码块之前,
// 必须先获得对account账户的锁定――其他线程无法获得锁,也就无法修改它
// 这种做法符合:“加锁 → 修改 → 释放锁”的逻辑
synchronized (account)
{
// 账户余额大于取钱数目
if (account.getBalance() >= drawAmount)
{
// 吐出钞票
System.out.println(getName()
+ "取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余额
account.setBalance(account.getBalance() - drawAmount);
System.out.println("\t余额为: " + account.getBalance());
}
else
{
System.out.println(getName() + "取钱失败!余额不足!");
}
}
//同步代码块结束,该线程释放同步锁
}
}

任何时候只有一个线程可以进入修改共享资源的代码区(临界区)。

同步方法

对于synchronized修饰的实例方法(非static方法),无需显式指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象。

使用同步方法可以非常方便的实现线程安全的类,该类的对象可以被多个线程安全地访问。

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
public class Account
{
// 封装账户编号、账户余额两个Field
private String accountNo;
private double balance;
public Account(){}
// 构造器
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}

// accountNo的setter和getter方法
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
// 因此账户余额不允许随便修改,所以只为balance提供getter方法,
public double getBalance()
{
return this.balance;
}

// 提供一个线程安全draw()方法来完成取钱操作
public synchronized void draw(double drawAmount)
{
// 账户余额大于取钱数目
if (balance >= drawAmount)
{
// 吐出钞票
System.out.println(Thread.currentThread().getName()
+ "取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余额
balance -= drawAmount;
System.out.println("\t余额为: " + balance);
}
else
{
System.out.println(Thread.currentThread().getName()
+ "取钱失败!余额不足!");
}
}

// 下面两个方法根据accountNo来重写hashCode()和equals()方法
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}

synchronized可以修饰代码块,方法,但不能修饰变量,构造器

释放同步监视器

无法显式释放,下面几种情况会释放对同步监视器的锁定:

  • 当前线程同步方法,同步代码块执行结束
  • 当前线程在同步方法,同步代码中遇到break,retrun终止了该代码块,该方法继续执行
  • 当前线程在同步方法,同步代码出现了未处理的Error或Exception
  • 当前线程在同步方法,同步代码中调用了同步检索器的wait()方法,则当前线程暂停,释放同步监视器。

下面情况不会释放:

  • 当前线程调用Thread.sleep(), Thread.yield()方法来暂停当前线程的执行
  • 其他线程调用了该线程的suspend()方法将该线程挂起。

同步锁(Lock)

通过显式定义同步锁对象来实现同步,这种机制下,同步锁由Lock对象充当。

某些锁可能允许对共享资源并发访问,如ReadWriteLock(读写锁),Lock,ReadWriteLock是Java 5的两个根接口,并为Lock提供了ReentrantLock(可重入锁)实现类,为ReadWriteLock提供了ReentrantReadWriteLock实现类。

比较常用的是ReentrantLock。可以显式的加锁,释放锁。

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
public class Account
{
// 定义锁对象
private final ReentrantLock lock = new ReentrantLock();
// 封装账户编号、账户余额两个Field
private String accountNo;
private double balance;
public Account(){}
// 构造器
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}

// accountNo的setter和getter方法
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
// 因此账户余额不允许随便修改,所以只为balance提供getter方法,
public double getBalance()
{
return this.balance;
}

// 提供一个线程安全draw()方法来完成取钱操作
public void draw(double drawAmount)
{
// 加锁
lock.lock();
try
{
// 账户余额大于取钱数目
if (balance >= drawAmount)
{
// 吐出钞票
System.out.println(Thread.currentThread().getName()
+ "取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余额
balance -= drawAmount;
System.out.println("\t余额为: " + balance);
}
else
{
System.out.println(Thread.currentThread().getName()
+ "取钱失败!余额不足!");
}
}
finally
{
// 修改完成,释放锁
lock.unlock();
}
}

// 下面两个方法根据accountNo来重写hashCode()和equals()方法
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}

对应一个Account对象,同一时刻只能有一个线程进入临界区。

当获取了多个锁时,必须已相反的顺序释放,且必须在所有锁被获取的相同的范围内释放所有锁。

ReetrantLock锁具有可重入性,也就是说一个线程可以对已被加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计数器来追踪lock()方法的嵌套调用。

死锁

两个线程互相等待对方释放同步监视器就会发生死锁。

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
class A
{
public synchronized void foo( B b )
{
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了A实例的foo方法" ); //①
try
{
Thread.sleep(200);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用B实例的last方法"); //③
b.last();
}
public synchronized void last()
{
System.out.println("进入了A类的last方法内部");
}
}
class B
{
public synchronized void bar( A a )
{
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了B实例的bar方法" ); //②
try
{
Thread.sleep(200);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用A实例的last方法"); //④
a.last();
}
public synchronized void last()
{
System.out.println("进入了B类的last方法内部");
}
}
public class DeadLock implements Runnable
{
A a = new A();
B b = new B();
public void init()
{
Thread.currentThread().setName("主线程");
// 调用a对象的foo方法
a.foo(b);
System.out.println("进入了主线程之后");
}
public void run()
{
Thread.currentThread().setName("副线程");
// 调用b对象的bar方法
b.bar(a);
System.out.println("进入了副线程之后");
}
public static void main(String[] args)
{
DeadLock dl = new DeadLock();
// 以dl为target启动新线程
new Thread(dl).start();
// 调用init()方法
dl.init();
}
}

线程通信

当线程在系统内运行时,线程调度具有一定的透明性,程序通常无法准确控制线程的轮换执行。但Java也提供了一些机制来保证线程协调运行。

传统的线程通信

Oject类提供了wait(), notify(), notifyAll()三个方法。这三个方法必须由同步监视器对象来调用,这可分为一下两种情况。

  • synchroninzed, 因为该类默认实例(this)就是同步监视器,所以可以在同步方法直接调用这三个方法。
  • 对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用这个三个方法(只适用于Synchronized同步的线程)。

  • wait(), 导致当前线程释放同步监视器并阻塞等待,直到其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程。可以带时间参数。

  • notify(): 唤醒在此同步监视器上等待的一个线程,如果有多个线程在等待(调用了wait方法的线程),则会随机选择唤醒其中一个线程,重新进入尝试获得锁的队列。
  • notiifyAll(): 唤醒在此同步监视器等待的所有线程(调用了wait方法的线程),重新进入尝试获得锁的队列。
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
public class Account
{
// 封装账户编号、账户余额两个Field
private String accountNo;
private double balance;
//标识账户中是否已有存款的旗标
private boolean flag = false;

public Account(){}
// 构造器
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}

// accountNo的setter和getter方法
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
// 因此账户余额不允许随便修改,所以只为balance提供getter方法,
public double getBalance()
{
return this.balance;
}

public synchronized void draw(double drawAmount)
{
try
{
// 如果flag为假,表明账户中还没有人存钱进去,取钱方法阻塞
if (!flag)
{
wait();
}
else
{
// 执行取钱
System.out.println(Thread.currentThread().getName()
+ " 取钱:" + drawAmount);
balance -= drawAmount;
System.out.println("账户余额为:" + balance);
// 将标识账户是否已有存款的旗标设为false。
flag = false;
// 唤醒其他线程
notifyAll();
}
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
}
public synchronized void deposit(double depositAmount)
{
try
{
// 如果flag为真,表明账户中已有人存钱进去,则存钱方法阻塞
if (flag) //①
{
wait();
}
else
{
// 执行存款
System.out.println(Thread.currentThread().getName()
+ " 存款:" + depositAmount);
balance += depositAmount;
System.out.println("账户余额为:" + balance);
// 将表示账户是否已有存款的旗标设为true
flag = true;
// 唤醒其他线程
notifyAll();
}
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
}

// 下面两个方法根据accountNo来重写hashCode()和equals()方法
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}

使用Condition控制线程通信

如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则不存在隐式的同步监视器,也就是不能用上面的方法进行线程通信了。

当使用Lock对象来保证同步时,Java提供了一个Condition类,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他处于等待的线程。

Condition实例被绑定在一个Lock对象上,可以通过调用Lock对象的newCondition()方法即可。Condition类提供了如下三个方法:

  • await(): 类似与隐式同步监视器上的wait()方法,导致当前线程释放锁并等待,直到其他线程调用该Condition的signal()方法或signalAll()方法来唤醒该线程。
  • signal(): 唤醒在此lock对象上等待的单个线程(获得锁又通过调用await()的放弃锁的线程)。如果有多个线程,则随机选择一个。
  • signalAll(): 唤醒在此lock对象上等待的所有线程(获得锁又通过调用await()的放弃锁的线程)。
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
public class Account
{
// 显式定义Lock对象
private final Lock lock = new ReentrantLock();
// 获得指定Lock对象对应的Condition
private final Condition cond = lock.newCondition();
// 封装账户编号、账户余额两个Field
private String accountNo;
private double balance;
//标识账户中是否已有存款的旗标
private boolean flag = false;

public Account(){}
// 构造器
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}

// accountNo的setter和getter方法
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
// 因此账户余额不允许随便修改,所以只为balance提供getter方法,
public double getBalance()
{
return this.balance;
}

public void draw(double drawAmount)
{
// 加锁
lock.lock();
try
{
// 如果flag为假,表明账户中还没有人存钱进去,取钱方法阻塞
if (!flag)
{
cond.wait();
}
else
{
// 执行取钱
System.out.println(Thread.currentThread().getName()
+ " 取钱:" + drawAmount);
balance -= drawAmount;
System.out.println("账户余额为:" + balance);
// 将标识账户是否已有存款的旗标设为false。
flag = false;
// 唤醒其他线程
cond.signalAll();
}
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 使用finally块来释放锁
finally
{
lock.unlock();
}
}
public void deposit(double depositAmount)
{
lock.lock();
try
{
// 如果flag为真,表明账户中已有人存钱进去,则存钱方法阻塞
if (flag) //①
{
cond.wait();
}
else
{
// 执行存款
System.out.println(Thread.currentThread().getName()
+ " 存款:" + depositAmount);
balance += depositAmount;
System.out.println("账户余额为:" + balance);
// 将表示账户是否已有存款的旗标设为true
flag = true;
// 唤醒其他线程
cond.signalAll();
}
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 使用finally块来释放锁
finally
{
lock.unlock();
}
}

// 下面两个方法根据accountNo来重写hashCode()和equals()方法
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}

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

BlockingQueue的主要用途是作为线程同步的工具,当生产者线程试图向BlockingQueue中放入元素时,如果队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果队列已空,则该线程被阻塞。

  • put(E e)
  • take()
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
class Producer extends Thread
{
private BlockingQueue<String> bq;
public Producer(BlockingQueue<String> bq)
{
this.bq = bq;
}
public void run()
{
String[] strArr = new String[]
{
"Java",
"Struts",
"Spring"
};
for (int i = 0 ; i < 999999999 ; i++ )
{
System.out.println(getName() + "生产者准备生产集合元素!");
try
{
Thread.sleep(200);
// 尝试放入元素,如果队列已满,线程被阻塞
bq.put(strArr[i % 3]);
}
catch (Exception ex){ex.printStackTrace();}
System.out.println(getName() + "生产完成:" + bq);
}
}
}
class Consumer extends Thread
{
private BlockingQueue<String> bq;
public Consumer(BlockingQueue<String> bq)
{
this.bq = bq;
}
public void run()
{
while(true)
{
System.out.println(getName() + "消费者准备消费集合元素!");
try
{
Thread.sleep(200);
// 尝试取出元素,如果队列已空,线程被阻塞
bq.take();
}
catch (Exception ex){ex.printStackTrace();}
System.out.println(getName() + "消费完成:" + bq);
}
}
}
public class BlockingQueueTest2
{
public static void main(String[] args)
{
// 创建一个容量为3的BlockingQueue
BlockingQueue<String> bq = new ArrayBlockingQueue<>(1);
// 启动3条生产者线程
new Producer(bq).start();
new Producer(bq).start();
new Producer(bq).start();
// 启动一条消费者线程
new Consumer(bq).start();
}
}

线程组和未处理的异常

线程池

线程相关类

1…131415…17
Plusaber

Plusaber

Plusaber's Blog
82 posts
12 categories
22 tags
Links
  • LinkedIn
  • Indeed
  • Baito
  • Kaggle
© 2014 – 2019 Plusaber
Powered by Hexo v3.8.0
|
Theme – NexT.Mist v7.1.1